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
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,17 @@ the specified screen as per the Figma document. Currently there is a dew

## Syncing your Figma Document

* Click Figma Bridge → Sync Document
* Enter your Personal Access Token (this will be stored in your Player Prefs for future use)
* It will ask if you want to use the current scene to generate prototype flow - Click yes
Click on the appropriate menu item under **Figma Bridge** to sync your document:

* **Figma Bridge → Sync ALL** - Complete sync including document download and all assets (recommended for initial setup)
* **Figma Bridge → Sync Document (No Image)** - Downloads only the document structure without downloading image assets
* **Figma Bridge → Reprocess Downloaded Document** - Reprocess the currently cached Figma document without re-downloading
* **Figma Bridge → Download Server Rendered Images** - Separately download complex vector shapes (useful for updating only rendered assets)
* **Figma Bridge → Download Image Fills** - Separately download image fills (useful for updating only image assets)

You will need to:
* Enter or confirm your Personal Access Token (this will be stored in your Player Prefs for future use)
* Choose whether you want to use the current scene to generate prototype flow when prompted

## Selecting Figma Pages

Expand All @@ -92,6 +100,12 @@ assets imported. Any page that is not selected will have the following rules:
| **Pages** | Prefabs of each complete page are stored in the *Pages* folder |
| **Vectors** | Rendered on the server as a PNG (see *Server Rendering* nelow) |

* **Placeholder Images** - When image downloads fail (due to network issues, rate limiting, etc.), placeholder images (2x2 gray PNG) are automatically created
* **Failed Downloads Can Be Retried** - Use the "Download Server Rendered Images" or "Download Image Fills" menu items to re-attempt downloads - placeholders are automatically replaced with actual images
* **No Broken References** - Placeholder images ensure your UI continues to render even if some images fail to download

This is particularly useful when dealing with the Figma API's rate limiting (429 errors) on documents with many server-rendered shapes.

## Fonts

With the goal of trying to make it 1-click sync, if the font doesnt exist in your project it will try and download a
Expand Down Expand Up @@ -167,6 +181,14 @@ uses reflection to do the following:
For example, I could add ```public TextMeshPro_UGUI Title``` and if there is a text object called *Title* then it will
be assigned to that field.

### Advanced Field Binding
The field binding system now supports multiple naming conventions, making it more flexible:
* **Exact Case-Insensitive Match** - "MyButton" field matches "mybutton" object
* **Normalized Name Matching** - "My_Play_Button" field matches "My Play Button" or "my-play-button" object (spaces, dashes, and underscores are ignored)
* **Underscore-Prefixed Fields** - Fields like "m_ScoreLabel" will match objects named "ScoreLabel"

This allows your Figma document to use natural naming (with spaces and dashes) that will be automatically matched to C# field names.

![Button Press Binding](/Docs/ButtonPressBinding.png)

* If you add the ```[[BindFigmaButtonPress("PlayButton")]]``` attribute to a method, then it will add an onClick
Expand Down
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
Loading