├── Resources └── Icon128.png ├── Content ├── WBP_FigmaFrame.uasset ├── Inter-VariableFont_slnt,wght.ttf ├── Inter-VariableFont_slnt_wght.uasset ├── Inter-VariableFont_slnt_wght_Font.uasset └── Python │ ├── runImportFigmaDoc.py │ └── figmaApi.py ├── Source └── FigmaImporter │ ├── Public │ ├── FigmaImporter.h │ └── FigmaImporterBPLibrary.h │ ├── Private │ ├── FigmaImporter.cpp │ └── FigmaImporterBPLibrary.cpp │ └── FigmaImporter.Build.cs ├── FigmaImporter.uplugin ├── LICENSE ├── .gitignore └── README.md /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phil-L-101/Figma2Unreal/HEAD/Resources/Icon128.png -------------------------------------------------------------------------------- /Content/WBP_FigmaFrame.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phil-L-101/Figma2Unreal/HEAD/Content/WBP_FigmaFrame.uasset -------------------------------------------------------------------------------- /Content/Inter-VariableFont_slnt,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phil-L-101/Figma2Unreal/HEAD/Content/Inter-VariableFont_slnt,wght.ttf -------------------------------------------------------------------------------- /Content/Inter-VariableFont_slnt_wght.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phil-L-101/Figma2Unreal/HEAD/Content/Inter-VariableFont_slnt_wght.uasset -------------------------------------------------------------------------------- /Content/Inter-VariableFont_slnt_wght_Font.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phil-L-101/Figma2Unreal/HEAD/Content/Inter-VariableFont_slnt_wght_Font.uasset -------------------------------------------------------------------------------- /Source/FigmaImporter/Public/FigmaImporter.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "Modules/ModuleManager.h" 6 | 7 | class FFigmaImporterModule : public IModuleInterface 8 | { 9 | public: 10 | 11 | /** IModuleInterface implementation */ 12 | virtual void StartupModule() override; 13 | virtual void ShutdownModule() override; 14 | }; 15 | -------------------------------------------------------------------------------- /Content/Python/runImportFigmaDoc.py: -------------------------------------------------------------------------------- 1 | import figmaApi 2 | from importlib import * 3 | reload(figmaApi) 4 | 5 | AccessToken = "" 6 | ImportDirectory = "/Game/" 7 | FileID = "" 8 | Pages = [-1] #List of pages to import (zero indexed), -1 imports all pages in the document 9 | FileDocument = figmaApi.FigmaDocument(FileID, ImportDirectory, AccessToken, Pages) 10 | FileDocument.writeToUnreal() 11 | 12 | 13 | -------------------------------------------------------------------------------- /FigmaImporter.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "0.1", 5 | "FriendlyName": "FigmaImporter", 6 | "Description": "Allows importing of Figma documents from API", 7 | "Category": "Importers", 8 | "CreatedBy": "Phil Littler", 9 | "CreatedByURL": "", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": false, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "FigmaImporter", 20 | "Type": "Runtime", 21 | "LoadingPhase": "PreLoadingScreen" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /Source/FigmaImporter/Private/FigmaImporter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "FigmaImporter.h" 4 | 5 | #define LOCTEXT_NAMESPACE "FFigmaImporterModule" 6 | 7 | void FFigmaImporterModule::StartupModule() 8 | { 9 | // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module 10 | 11 | } 12 | 13 | void FFigmaImporterModule::ShutdownModule() 14 | { 15 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 16 | // we call this function before unloading the module. 17 | 18 | } 19 | 20 | #undef LOCTEXT_NAMESPACE 21 | 22 | IMPLEMENT_MODULE(FFigmaImporterModule, FigmaImporter) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Phil-L 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | 22 | # Compiled Static libraries 23 | *.lai 24 | *.la 25 | *.a 26 | *.lib 27 | 28 | # Executables 29 | *.exe 30 | *.out 31 | *.app 32 | *.ipa 33 | 34 | # These project files can be generated by the engine 35 | *.xcodeproj 36 | *.xcworkspace 37 | *.sln 38 | *.suo 39 | *.opensdf 40 | *.sdf 41 | *.VC.db 42 | *.VC.opendb 43 | 44 | # Precompiled Assets 45 | SourceArt/**/*.png 46 | SourceArt/**/*.tga 47 | 48 | # Binary Files 49 | Binaries/* 50 | Plugins/*/Binaries/* 51 | 52 | # Builds 53 | Build/* 54 | 55 | # Whitelist PakBlacklist-.txt files 56 | !Build/*/ 57 | Build/*/** 58 | !Build/*/PakBlacklist*.txt 59 | 60 | # Don't ignore icon files in Build 61 | !Build/**/*.ico 62 | 63 | # Built data for maps 64 | *_BuiltData.uasset 65 | 66 | # Configuration files generated by the Editor 67 | Saved/* 68 | 69 | # Compiled source files for the engine to use 70 | Intermediate/* 71 | Plugins/*/Intermediate/* 72 | 73 | # Cache files for the editor to use 74 | DerivedDataCache/* 75 | /Content/Python/__pycache__/* 76 | -------------------------------------------------------------------------------- /Source/FigmaImporter/FigmaImporter.Build.cs: -------------------------------------------------------------------------------- 1 | // Some copyright should be here... 2 | 3 | using UnrealBuildTool; 4 | 5 | public class FigmaImporter : ModuleRules 6 | { 7 | public FigmaImporter(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicIncludePaths.AddRange( 12 | new string[] { 13 | // ... add public include paths required here ... 14 | } 15 | ); 16 | 17 | 18 | PrivateIncludePaths.AddRange( 19 | new string[] { 20 | // ... add other private include paths required here ... 21 | } 22 | ); 23 | 24 | 25 | PublicDependencyModuleNames.AddRange( 26 | new string[] 27 | { 28 | "Core", 29 | "UMGEditor", 30 | "UMG" 31 | // ... add other public dependencies that you statically link with here ... 32 | } 33 | ); 34 | 35 | 36 | PrivateDependencyModuleNames.AddRange( 37 | new string[] 38 | { 39 | "CoreUObject", 40 | "Engine", 41 | "Slate", 42 | "SlateCore" 43 | // ... add private dependencies that you statically link with here ... 44 | } 45 | ); 46 | 47 | 48 | DynamicallyLoadedModuleNames.AddRange( 49 | new string[] 50 | { 51 | // ... add any modules that your module loads dynamically here ... 52 | } 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Figma2Unreal 2 | [Demonstration Video](https://youtu.be/E9DOSqHd72I) 3 | 4 | Figma2Unreal uses the [Figma API](https://www.figma.com/developers/api) and python scripts to take assets created in Figma and create nested UMG widgets suitable for making more advanced prototypes. 5 | 6 | It is rudimentary at the moment so complex Figma functionality is not maintained, but it should still speed up the process with some manual cleanup required. 7 | 8 | ## Installation and Usage 9 | * Install the plugin to your Unreal Project and make sure you have Python scripting enabled. 10 | * Use pip install requests to install the requests module to your Unreal Python environment {IMPORTANT: Must be your Unreal python envinronment found in "Engine/Binaries/ThirdParty/Python3" folder 11 | * Edit "Content/Python/runImportFigmaDoc.py" to use your [Personal Access Token](https://www.figma.com/developers/api#access-tokens), [FileID](https://www.figma.com/developers/api#files-endpoints), and desired import directory. 12 | * Run "runImportFigmaDoc.py" from inside Unreal and you should get sub-folders with all of your newly created widgets and asssets 13 | * Restart Unreal to have all of your referenced widgets update in their parents (seems to be a new-ish bug with Unreal that editing User Widgets isn't reflected in parents until restart) 14 | 15 | 16 | ## Known Issues 17 | * Only supports a single canvas 18 | * Reimporting overrides assets and brings up dialogue boxes for each overrides 19 | * Doesn't seem to release access to some widgets so deleting/editing widgets immedietly after running can cause issues 20 | * Requires restart to update widgets in their parents (think this is an Unreal bug) 21 | * Doesn't support gradient fills 22 | * Doesn't support outlines 23 | * Doesn't support advanced layouts 24 | * Doesn't use text justification 25 | * Text doesn't wrap or set up spacing correctly 26 | * Would like to support multiple fonts (could download google fonts with separate API) 27 | * Would idealy like to use native unreal materials for rectangles and lines to allow more flexible editibility 28 | * Would like to use native Unreal layouts for grids, horizontal, vertical boxes etx 29 | -------------------------------------------------------------------------------- /Source/FigmaImporter/Public/FigmaImporterBPLibrary.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | #include "CoreMinimal.h" 5 | #include "UObject/Object.h" 6 | #include "Runtime/CoreUObject/Public/UObject/UObjectGlobals.h" 7 | 8 | 9 | //~~~~~~~~~~~~ UMG ~~~~~~~~~~~~~~~ 10 | #include "Runtime/UMG/Public/UMG.h" 11 | #include "Runtime/UMG/Public/UMGStyle.h" 12 | #include "Runtime/UMG/Public/Slate/SObjectWidget.h" 13 | #include "Runtime/UMG/Public/IUMGModule.h" 14 | #include "Runtime/UMG/Public/Blueprint/WidgetTree.h" 15 | #include "Runtime/UMG/Public/Blueprint/UserWidget.h" 16 | #include "Editor/UMGEditor/Public/WidgetBlueprint.h" 17 | #include "Runtime/SlateCore/Public/Fonts/SlateFontInfo.h" 18 | #include "Runtime/Engine/Classes/Engine/Texture2D.h" 19 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 20 | #include "Kismet/BlueprintFunctionLibrary.h" 21 | #include "FigmaImporterBPLibrary.generated.h" 22 | 23 | /* 24 | * Function library class. 25 | * Each function in it is expected to be static and represents blueprint node that can be called in any blueprint. 26 | * 27 | * When declaring function you can define metadata for the node. Key function specifiers will be BlueprintPure and BlueprintCallable. 28 | * BlueprintPure - means the function does not affect the owning object in any way and thus creates a node without Exec pins. 29 | * BlueprintCallable - makes a function which can be executed in Blueprints - Thus it has Exec pins. 30 | * DisplayName - full name of the node, shown when you mouse over the node and in the blueprint drop down menu. 31 | * Its lets you name the node using characters not allowed in C++ function names. 32 | * CompactNodeTitle - the word(s) that appear on the node. 33 | * Keywords - the list of keywords that helps you to find node when you search for it using Blueprint drop-down menu. 34 | * Good example is "Print String" node which you can find also by using keyword "log". 35 | * Category - the category your node will be under in the Blueprint drop-down menu. 36 | * 37 | * For more info on custom blueprint nodes visit documentation: 38 | * https://wiki.unrealengine.com/Custom_Blueprint_Node_Creation 39 | */ 40 | UCLASS() 41 | class UFigmaImporterBPLibrary : public UBlueprintFunctionLibrary 42 | { 43 | GENERATED_UCLASS_BODY() 44 | 45 | UFUNCTION(BlueprintCallable) 46 | static void SetWidgetSizeAndPosition(UWidget* Widget, FVector2D Size, FVector2D Position, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors); 47 | 48 | UFUNCTION(BlueprintCallable) 49 | static void ClearContent(UWidgetBlueprint* Widget); 50 | 51 | UFUNCTION(BlueprintCallable) 52 | static void SetBackground(UWidgetBlueprint* Widget, FVector2D Size, FLinearColor Color); 53 | 54 | UFUNCTION(BlueprintCallable) 55 | static UUserWidget* AddChildWidget(UWidgetBlueprint* Widget, FString ChildClassPath, FName ChildWidgetName, FVector2D Size, FVector2D Position, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors); 56 | 57 | UFUNCTION(BlueprintCallable) 58 | static UImage* AddImageWidget(UWidgetBlueprint* Widget, FName Name, FVector2D Size, FVector2D Position, FString ImagePath, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors); 59 | 60 | UFUNCTION(BlueprintCallable) 61 | static UImage* AddRectangleWidget(UWidgetBlueprint* Widget, FName Name, FVector2D Size, FVector2D Position, FLinearColor Color, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors); 62 | 63 | UFUNCTION(BlueprintCallable) 64 | static UTextBlock* AddTextWidget(UWidgetBlueprint* Widget, FName Name, FString Content, FSlateFontInfo Font, FVector2D Size, FVector2D Position, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors); 65 | }; 66 | -------------------------------------------------------------------------------- /Source/FigmaImporter/Private/FigmaImporterBPLibrary.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | 4 | #include "FigmaImporterBPLibrary.h" 5 | #include "FigmaImporter.h" 6 | 7 | UFigmaImporterBPLibrary::UFigmaImporterBPLibrary(const FObjectInitializer& ObjectInitializer) 8 | : Super(ObjectInitializer) 9 | { 10 | 11 | } 12 | 13 | // Helper function to set slot properties of a widget 14 | void UFigmaImporterBPLibrary::SetWidgetSizeAndPosition(UWidget* Widget, FVector2D Size, FVector2D Position, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors) 15 | { 16 | if (UCanvasPanelSlot* Slot = Cast(Widget->Slot)) { 17 | Slot->SetAnchors(FAnchors(MinAnchors[0], MinAnchors[1], MaxAnchors[0], MinAnchors[1])); 18 | Slot->SetAlignment(Alignment); 19 | Slot->SetSize(Size); 20 | Slot->SetPosition(Position); 21 | } 22 | 23 | else { 24 | UE_LOG(LogTemp, Warning, TEXT("Couldn't size widget")); 25 | } 26 | } 27 | 28 | // Clears all of the children from the content panel in the Frame template widget 29 | void UFigmaImporterBPLibrary::ClearContent(UWidgetBlueprint* Widget) 30 | { 31 | if(IsValid(Widget)) { 32 | Widget->Modify(); // Ensures asset is marked dirty for saving 33 | auto WidgetTree = Widget->WidgetTree; 34 | auto Canvas = Cast(WidgetTree->FindWidget("ContentPanel")); 35 | Canvas->ClearChildren(); 36 | } 37 | 38 | else { 39 | UE_LOG(LogTemp, Warning, TEXT("Couldn't clear content from widget as widget not valid")); 40 | } 41 | } 42 | 43 | // Sets background colour and size of Frame template widget 44 | void UFigmaImporterBPLibrary::SetBackground(UWidgetBlueprint* Widget, FVector2D Size, FLinearColor Color) 45 | { 46 | if (IsValid(Widget)) { 47 | Widget->Modify(); // Ensures asset is marked dirty for saving 48 | auto WidgetTree = Widget->WidgetTree; 49 | auto Canvas = Cast(WidgetTree->FindWidget("BackgroundContainer")); 50 | auto BackgroundImage = Cast(WidgetTree->FindWidget("BackgroundImage")); 51 | SetWidgetSizeAndPosition(Canvas, Size, FVector2D(0, 0), FVector2D(0.5, 0.5), FVector2D(0.5, 0.5), FVector2D(0.5, 0.5)); 52 | BackgroundImage->SetColorAndOpacity(Color); 53 | } 54 | else{ 55 | UE_LOG(LogTemp, Warning, TEXT("Couldn't set background for frame as widget not valid")); 56 | } 57 | } 58 | 59 | // Creates an instance of a custom UserWidget from a class path and adds it to the parent Frame template 60 | UUserWidget* UFigmaImporterBPLibrary::AddChildWidget(UWidgetBlueprint* Widget, FString ChildClassPath, FName ChildWidgetName, FVector2D Size, FVector2D Position, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors) 61 | { 62 | if(IsValid(Widget)) { 63 | Widget->Modify(); // Ensures asset is marked dirty for saving 64 | 65 | FString BaseText = TEXT("WidgetBlueprint '"); 66 | FString FullPath = BaseText.Append(ChildClassPath).Append(TEXT("'")); 67 | if (UClass* ChildBPWidgetClass = LoadClass(NULL, *FullPath)) 68 | { 69 | auto WidgetTree = Widget->WidgetTree; 70 | UUserWidget* ChildWidget = WidgetTree->ConstructWidget(ChildBPWidgetClass, ChildWidgetName); 71 | auto Canvas = Cast(WidgetTree->FindWidget("ContentPanel")); 72 | Canvas->AddChild(ChildWidget); 73 | SetWidgetSizeAndPosition(ChildWidget, Size, Position, Alignment, MinAnchors, MaxAnchors); 74 | return ChildWidget; 75 | } 76 | else { 77 | return NULL; 78 | } 79 | } 80 | else{ 81 | UE_LOG(LogTemp, Warning, TEXT("Couldn't add child widget to frame as frame not valid")); 82 | return NULL; 83 | } 84 | } 85 | 86 | // Adds a basic image to a parent Frame template using a path to a Texture2D asset 87 | UImage* UFigmaImporterBPLibrary::AddImageWidget(UWidgetBlueprint* Widget, FName Name, FVector2D Size, FVector2D Position, FString ImagePath, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors) 88 | { 89 | if (IsValid(Widget)) { 90 | Widget->Modify(); // Ensures asset is marked dirty for saving 91 | auto WidgetTree = Widget->WidgetTree; 92 | UImage* Image = WidgetTree->ConstructWidget(UImage::StaticClass(), Name); 93 | auto Canvas = Cast(WidgetTree->FindWidget("ContentPanel")); 94 | Canvas->AddChild(Image); 95 | UTexture2D* Texture = LoadObject(NULL, *ImagePath, NULL, LOAD_None, NULL); 96 | Image->SetBrushFromTexture(Texture, false); 97 | SetWidgetSizeAndPosition(Image, Size, Position, Alignment, MinAnchors, MaxAnchors); 98 | return Image; 99 | } 100 | else{ 101 | UE_LOG(LogTemp, Warning, TEXT("Couldn't add image to frame as frame not valid")); 102 | return NULL; 103 | } 104 | } 105 | 106 | // Adds a basic image to parent Frame template with no texture but a colour 107 | UImage* UFigmaImporterBPLibrary::AddRectangleWidget(UWidgetBlueprint* Widget, FName Name, FVector2D Size, FVector2D Position, FLinearColor LinearColor, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors) 108 | { 109 | if (IsValid(Widget)) { 110 | Widget->Modify(); // Ensures asset is marked dirty for saving 111 | auto WidgetTree = Widget->WidgetTree; 112 | UImage* Image = WidgetTree->ConstructWidget(UImage::StaticClass(), Name); 113 | auto Canvas = Cast(WidgetTree->FindWidget("ContentPanel")); 114 | Canvas->AddChild(Image); 115 | Image->SetColorAndOpacity(LinearColor); 116 | SetWidgetSizeAndPosition(Image, Size, Position, Alignment, MinAnchors, MaxAnchors); 117 | return Image; 118 | } 119 | else{ 120 | UE_LOG(LogTemp, Warning, TEXT("Couldn't add rectangle to frame as frame not valid")); 121 | return NULL; 122 | } 123 | } 124 | 125 | // Adds text to a parent Frame template 126 | UTextBlock* UFigmaImporterBPLibrary::AddTextWidget(UWidgetBlueprint* Widget, FName Name, FString Content, FSlateFontInfo Font, FVector2D Size, FVector2D Position, FVector2D Alignment, FVector2D MinAnchors, FVector2D MaxAnchors) 127 | { 128 | if (IsValid(Widget)) { 129 | Widget->Modify(); // Ensures asset is marked dirty for saving 130 | auto WidgetTree = Widget->WidgetTree; 131 | UTextBlock* TextBlock = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), Name); 132 | auto Canvas = Cast(WidgetTree->FindWidget("ContentPanel")); 133 | Canvas->AddChild(TextBlock); 134 | TextBlock->SetText(FText::AsCultureInvariant(Content)); 135 | TextBlock->SetFont(Font); 136 | SetWidgetSizeAndPosition(TextBlock, Size, Position, Alignment, MinAnchors, MaxAnchors); 137 | return TextBlock; 138 | } 139 | else{ 140 | UE_LOG(LogTemp, Warning, TEXT("Couldn't text to frame as frame not valid")); 141 | return NULL; 142 | } 143 | } 144 | 145 | -------------------------------------------------------------------------------- /Content/Python/figmaApi.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import unreal 4 | import os 5 | 6 | # Helper function to check if an Unreal directory exists and make it if not 7 | def createDirSafe(DirectoryPath): 8 | if not unreal.EditorAssetLibrary.does_directory_exist(DirectoryPath): 9 | unreal.EditorAssetLibrary.make_directory(DirectoryPath) 10 | 11 | # Helper function to sanitise name to use with unreal paths 12 | def sanitiseName(Name): 13 | SanitisedName = (Name.replace(" ", "_"). 14 | replace("-", "_"). 15 | replace("(", ""). 16 | replace(")", ""). 17 | replace(".", "_"). 18 | replace("\"", ""). 19 | replace(",", ""). 20 | replace("\'", ""). 21 | replace("“", ""). 22 | replace("”", ""). 23 | replace("[",""). 24 | replace("]",""). 25 | replace("=", ""). 26 | replace("#", ""). 27 | splitlines(False)[0][:50]) 28 | return SanitisedName 29 | 30 | # Gets full path to bluerint class 31 | def getAssetPath(Directory, AssetName): 32 | Path = Directory + AssetName + "." + AssetName + "_C" 33 | return Path 34 | 35 | # Create unique instance name to avoid conflicts 36 | def createInsatanceName(Name, id): 37 | return Name + "_" + id 38 | 39 | # Convert Fills into a single colour 40 | def getColorFromFills(Fills, DefaultColor): 41 | try: # May not have a background colour 42 | Color = { 43 | "r" : Fills[0]["color"]["r"], 44 | "g" : Fills[0]["color"]["g"], 45 | "b" : Fills[0]["color"]["b"], 46 | } 47 | try: # Only has opacity attribute if opacity not 1 48 | Color["a"] = Fills[0]["opacity"] 49 | except: 50 | Color["a"] = 1 51 | except: 52 | Color = DefaultColor 53 | return Color 54 | 55 | # Helper function to save assets 56 | def saveAsset(Asset): 57 | unreal.EditorAssetLibrary.save_loaded_asset(Asset, only_if_is_dirty=False) 58 | 59 | # Figma API uses absolute positions, we want relative positions 60 | def createRelativePosition(ParentPosition, ChildPosition): 61 | RelativePosition = [ChildPosition[0]-ParentPosition[0],ChildPosition[1]-ParentPosition[1]] 62 | return RelativePosition 63 | 64 | # Function to create a new frame asset from the template file 65 | def createFigmaFrameAsset(AssetPath): 66 | if unreal.EditorAssetLibrary.does_asset_exist(AssetPath): 67 | return unreal.EditorAssetLibrary.load_asset(AssetPath) 68 | else: 69 | FrameAsset = unreal.EditorAssetLibrary.duplicate_asset("/FigmaImporter/WBP_FigmaFrame",AssetPath) 70 | unreal.EditorAssetLibrary.save_loaded_asset(FrameAsset) 71 | return FrameAsset 72 | 73 | # Function to run through all the children of a frame and create assets for them recursively 74 | def createChildren(ChildrenDict, Document, Parent): 75 | Children = [] 76 | # Create a progress bar 77 | TotalFrames = len(ChildrenDict) 78 | DialogLabel = "Creating Children for: " + Parent.Name 79 | with unreal.ScopedSlowTask(TotalFrames, DialogLabel) as slow_task: 80 | slow_task.make_dialog(True) #Show Progress bar 81 | 82 | # Loop through all children and check the asset type 83 | for child in ChildrenDict: 84 | if child["type"] == "FRAME": 85 | ChildNode = FigmaFrame(Document,child, Parent) 86 | ChildNode.writeToUnreal() 87 | Children.append(ChildNode ) 88 | elif child["type"] == "GROUP": 89 | ChildNode = FigmaFrame(Document,child, Parent) 90 | ChildNode.writeToUnreal() 91 | Children.append(ChildNode ) 92 | elif child["type"] == "COMPONENT_SET": 93 | ChildNode = FigmaComponent(Document,child, Parent) 94 | ChildNode.writeToUnreal() 95 | Children.append(ChildNode ) 96 | elif child["type"] == "COMPONENT": 97 | ChildNode = FigmaComponent(Document,child, Parent) 98 | ChildNode.writeToUnreal() 99 | Children.append(ChildNode ) 100 | elif child["type"] == "INSTANCE": 101 | ChildNode = FigmaInstance(Document,child, Parent) 102 | Document.ComponentInstances.append(ChildNode) 103 | Children.append(ChildNode ) 104 | elif child["type"] == "VECTOR": 105 | ChildNode = FigmaVector(Document,child, Parent) 106 | ChildNode.writeToUnreal() 107 | Children.append(ChildNode ) 108 | elif child["type"] == "LINE": 109 | ChildNode = FigmaVector(Document,child, Parent) 110 | ChildNode.writeToUnreal() 111 | Children.append(ChildNode ) 112 | elif child["type"] == "BOOLEAN_OPERATION": 113 | ChildNode = FigmaVector(Document,child, Parent) 114 | ChildNode.writeToUnreal() 115 | Children.append(ChildNode ) 116 | elif child["type"] == "ELLIPSE": 117 | ChildNode = FigmaVector(Document,child, Parent) 118 | ChildNode.writeToUnreal() 119 | Children.append(ChildNode ) 120 | elif child["type"] == "REGULAR_POLYGON": 121 | ChildNode = FigmaVector(Document,child, Parent) 122 | ChildNode.writeToUnreal() 123 | Children.append(ChildNode ) 124 | elif child["type"] == "STAR": 125 | ChildNode = FigmaVector(Document,child, Parent) 126 | ChildNode.writeToUnreal() 127 | Children.append(ChildNode ) 128 | elif child["type"] == "RECTANGLE": 129 | ChildNode = FigmaVector(Document,child, Parent) 130 | ChildNode.writeToUnreal() 131 | Children.append(ChildNode ) 132 | elif child["type"] == "TEXT": 133 | ChildNode = FigmaText(Document,child, Parent) 134 | ChildNode.writeToUnreal() 135 | Children.append(ChildNode ) 136 | if slow_task.should_cancel(): # True if the user has pressed Cancel in the UI 137 | break 138 | slow_task.enter_progress_frame(1) # Advance progress by one frame. 139 | return Children 140 | 141 | # Top level parent for a Figma document 142 | class FigmaDocument(): 143 | def __init__(self, FileID, BaseDirectory, AccessToken, Pages = [-1]): 144 | self.FileID = FileID 145 | self.BaseDirectory = BaseDirectory 146 | self.AccessToken = AccessToken 147 | self.Components = {} 148 | self.ComponentInstances = [] 149 | self.Pages= Pages 150 | self.readFile() 151 | 152 | # Read in file from API 153 | def readFile(self): 154 | FileResponse=requests.get("https://api.figma.com/v1/files/" + self.FileID, headers={"X-Figma-Token": self.AccessToken}) 155 | self.File = json.loads(FileResponse.text) 156 | self.Name = sanitiseName(self.File["name"]) 157 | self.Directory = self.BaseDirectory + self.Name +"/" 158 | self.Canvases = [] 159 | for i, CanvasContent in enumerate(self.File["document"]["children"]): 160 | if i in self.Pages or self.Pages[0] == -1: 161 | self.Canvases.append(FigmaCanvas(CanvasContent, self)) 162 | 163 | 164 | 165 | # Produce assets from file in Unreal 166 | def writeToUnreal(self): 167 | createDirSafe(self.Directory) 168 | for Canvas in self.Canvases: 169 | createDirSafe(Canvas.Directory) 170 | AssetName = "WBP_" + Canvas.Name 171 | Canvas.FrameAsset = createFigmaFrameAsset(Canvas.Directory+AssetName) # Create new frame asset to use 172 | 173 | unreal.FigmaImporterBPLibrary.clear_content(Canvas.FrameAsset) # Clear any existing content from content tree (does not delete blueprint code but may break references) 174 | unreal.FigmaImporterBPLibrary.set_background(Canvas.FrameAsset, [10000,10000], [Canvas.BackgroundColor["r"],Canvas.BackgroundColor["g"],Canvas.BackgroundColor["b"],Canvas.BackgroundColor["a"]]) # Canvas Background set to arbitrary size of 10000x10000px 175 | Canvas.Children = createChildren(Canvas.ChildrenDict, self, Canvas) # Recursively creates child assets 176 | for Instance in self.ComponentInstances: 177 | Instance.writeToUnreal() 178 | saveAsset(Canvas.FrameAsset) 179 | 180 | # Add a component to the document that can be instanced 181 | def addComponent(self,Component): 182 | self.Components[Component.id] = Component 183 | 184 | 185 | class FigmaCanvas(): 186 | def __init__(self,CanvasContent, Document): 187 | self.Document = Document 188 | self.CanvasContent = CanvasContent 189 | self.Name = sanitiseName(self.CanvasContent["name"]) 190 | try: # May not have a background colour 191 | self.BackgroundColor = { 192 | "r" : self.CanvasContent["backgroundColor"]["r"], 193 | "g" : self.CanvasContent["backgroundColor"]["g"], 194 | "b" : self.CanvasContent["backgroundColor"]["b"], 195 | "a" : self.CanvasContent["backgroundColor"]["a"] 196 | } 197 | except KeyError: 198 | self.BackgroundColor = { 199 | "r" : 0.5, 200 | "g" : 0.5, 201 | "b" : 0.5, 202 | "a" : 1 203 | } 204 | self.ChildrenDict = self.CanvasContent["children"] 205 | self.xPosition = 0 206 | self.yPosition = 0 207 | self.Directory = self.Document.Directory+self.Name+"/" 208 | 209 | # Standard Figma frame holds children 210 | class FigmaFrame(): 211 | def __init__(self, Document, FrameDict, Parent): 212 | self.Document = Document 213 | self.Parent = Parent 214 | self.Name = sanitiseName(FrameDict["name"]) 215 | self.Directory = self.Parent.Directory + self.Name + "/" 216 | self.Height = FrameDict["absoluteBoundingBox"]["height"] 217 | self.Width = FrameDict["absoluteBoundingBox"]["width"] 218 | self.xPosition = FrameDict["absoluteBoundingBox"]["x"] 219 | self.yPosition = FrameDict["absoluteBoundingBox"]["y"] 220 | self.id = FrameDict["id"] 221 | if type(self.Parent) == FigmaCanvas: # Check if parent is the canvas and set anchors accordingly 222 | self.MinAnchors = [0.5,0.5] 223 | self.MaxAnchors = [0.5,0.5] 224 | self.Alignment = [0,0] 225 | else: 226 | self.MinAnchors = [0,0] 227 | self.MaxAnchors = [0,0] 228 | self.Alignment = [0,0] 229 | 230 | self.ChildrenDict = FrameDict["children"] 231 | 232 | self.BackgroundColor = getColorFromFills(FrameDict["fills"],{ 233 | "r" : 0, 234 | "g" : 0, 235 | "b" : 0, 236 | "a" : 0 237 | }) 238 | 239 | 240 | # Produce assets from frame in Unreal and add to parent frame 241 | def writeToUnreal(self): 242 | 243 | createDirSafe(self.Directory) 244 | 245 | self.AssetName = "WBP_" + self.Name 246 | 247 | self.FrameAsset = createFigmaFrameAsset(self.Directory+"/"+ self.AssetName) 248 | 249 | unreal.FigmaImporterBPLibrary.clear_content(self.FrameAsset) 250 | unreal.FigmaImporterBPLibrary.set_background(self.FrameAsset, [self.Width,self.Height], [self.BackgroundColor["r"],self.BackgroundColor["g"],self.BackgroundColor["b"],self.BackgroundColor["a"]]) 251 | self.Children = createChildren(self.ChildrenDict, self.Document, self) 252 | saveAsset(self.FrameAsset) 253 | unreal.FigmaImporterBPLibrary.add_child_widget(self.Parent.FrameAsset,getAssetPath(self.Directory, self.AssetName) , createInsatanceName(self.Name, self.id), [self.Width,self.Height], createRelativePosition([self.Parent.xPosition, self.Parent.yPosition], [self.xPosition, self.yPosition]), self.Alignment, self.MinAnchors, self.MaxAnchors) 254 | saveAsset(self.Parent.FrameAsset) 255 | 256 | # Special frame class that can be instanced 257 | class FigmaComponent(FigmaFrame): 258 | # Produce assets from frame in Unreal and add to parent frame 259 | def writeToUnreal(self): 260 | 261 | createDirSafe(self.Directory) 262 | 263 | self.AssetName = "WBP_" + self.Name 264 | 265 | self.FrameAsset = createFigmaFrameAsset(self.Directory+"/"+ self.AssetName) 266 | 267 | unreal.FigmaImporterBPLibrary.clear_content(self.FrameAsset) 268 | unreal.FigmaImporterBPLibrary.set_background(self.FrameAsset, [self.Width,self.Height], [self.BackgroundColor["r"],self.BackgroundColor["g"],self.BackgroundColor["b"],self.BackgroundColor["a"]]) 269 | self.Children = createChildren(self.ChildrenDict, self.Document, self) 270 | saveAsset(self.FrameAsset) 271 | unreal.FigmaImporterBPLibrary.add_child_widget(self.Parent.FrameAsset,getAssetPath(self.Directory, self.AssetName) , createInsatanceName(self.Name, self.id), [self.Width,self.Height], createRelativePosition([self.Parent.xPosition, self.Parent.yPosition], [self.xPosition, self.yPosition]), self.Alignment, self.MinAnchors, self.MaxAnchors) 272 | saveAsset(self.Parent.FrameAsset) 273 | self.Document.addComponent(self) 274 | 275 | # An instance that references an existing component 276 | class FigmaInstance(): 277 | def __init__(self, Document, FrameDict, Parent): 278 | self.Document = Document 279 | self.Parent = Parent 280 | self.Name = sanitiseName(FrameDict["name"]) 281 | self.Directory = self.Parent.Directory + self.Name + "/" 282 | self.Height = FrameDict["absoluteBoundingBox"]["height"] 283 | self.Width = FrameDict["absoluteBoundingBox"]["width"] 284 | self.xPosition = FrameDict["absoluteBoundingBox"]["x"] 285 | self.yPosition = FrameDict["absoluteBoundingBox"]["y"] 286 | self.id = FrameDict["id"] 287 | self.ComponentID = FrameDict["componentId"] 288 | if type(self.Parent) == FigmaCanvas: # Check if parent is the canvas and set anchors accordingly 289 | self.MinAnchors = [0.5,0.5] 290 | self.MaxAnchors = [0.5,0.5] 291 | self.Alignment = [0,0] 292 | else: 293 | self.MinAnchors = [0,0] 294 | self.MaxAnchors = [0,0] 295 | self.Alignment = [0,0] 296 | 297 | self.ChildrenDict = FrameDict["children"] 298 | 299 | self.BackgroundColor = getColorFromFills(FrameDict["fills"],{ 300 | "r" : 0, 301 | "g" : 0, 302 | "b" : 0, 303 | "a" : 0 304 | }) 305 | 306 | 307 | # Produce assets from frame in Unreal and add to parent frame 308 | def writeToUnreal(self): 309 | try: # Try finding source component in document 310 | InstanceSource = self.Document.Components[self.ComponentID] 311 | InstanceSourceDirectory = InstanceSource.Directory 312 | InstanceSourceAssetName = InstanceSource.AssetName 313 | unreal.FigmaImporterBPLibrary.add_child_widget(self.Parent.FrameAsset,getAssetPath(InstanceSourceDirectory, InstanceSourceAssetName) , createInsatanceName(self.Name, self.id), [self.Width,self.Height], createRelativePosition([self.Parent.xPosition, self.Parent.yPosition], [self.xPosition, self.yPosition]), self.Alignment, self.MinAnchors, self.MaxAnchors) 314 | saveAsset(self.Parent.FrameAsset) 315 | except KeyError: 316 | print("Unable to find source component") 317 | 318 | # A vector is the base class for all shapes and images in Figma 319 | class FigmaVector(): 320 | def __init__(self, Document, VectorDict, Parent): 321 | self.Document = Document 322 | self.Name = sanitiseName(VectorDict["name"]) 323 | self.Height = VectorDict["absoluteBoundingBox"]["height"] 324 | self.Width = VectorDict["absoluteBoundingBox"]["width"] 325 | self.xPosition = VectorDict["absoluteBoundingBox"]["x"] 326 | self.yPosition = VectorDict["absoluteBoundingBox"]["y"] 327 | self.id = VectorDict["id"] 328 | self.MinAnchors = [0,0] 329 | self.MaxAnchors = [0,0] 330 | self.Alignment = [0,0] 331 | self.Parent = Parent 332 | 333 | # Uses Figma API to download a bitmap image representation of the vector (Special vectors like rectangles should use native Unreal assets where possible but not implemented yet) 334 | def downloadImage(self): 335 | FileResponse=requests.get("https://api.figma.com/v1/images/" + self.Document.FileID + "?ids=" + self.id, headers={"X-Figma-Token": self.Document.AccessToken}) 336 | File = json.loads(FileResponse.text) 337 | ImageURL = File["images"][self.id] 338 | ImageData = requests.get(ImageURL).content 339 | 340 | self.RawImageDirectory = unreal.Paths.project_dir() + "RawAssets" + self.Parent.Directory 341 | if not os.path.exists(self.RawImageDirectory): 342 | os.makedirs(self.RawImageDirectory) 343 | self.ImageName = "T_" + self.Name 344 | self.RawImageName = self.ImageName + ".png" 345 | self.RawImagePath = self.RawImageDirectory + self.RawImageName 346 | self.ImageDirectory = self.Parent.Directory +"Images/" 347 | createDirSafe(self.ImageDirectory) 348 | self.ImagePath = os.path.splitext(self.ImageDirectory + self.RawImageName)[0] 349 | with open(self.RawImagePath, 'wb') as handler: 350 | handler.write(ImageData) 351 | 352 | # Produce assets and add to parent frame 353 | def writeToUnreal(self): 354 | self.downloadImage() 355 | AssetTools = unreal.AssetToolsHelpers.get_asset_tools() 356 | AssetImportTask = unreal.AssetImportTask() 357 | AssetImportTask.set_editor_property('filename', self.RawImagePath) 358 | AssetImportTask.set_editor_property('destination_path', self.ImageDirectory) 359 | AssetImportTask.set_editor_property('replace_existing', True) 360 | AssetImportTask.set_editor_property('replace_existing_settings', True) 361 | AssetImportTask.set_editor_property('save', True) 362 | AssetTools.import_asset_tasks([AssetImportTask]) 363 | ImageTexture2D = unreal.EditorAssetLibrary.load_asset(self.ImagePath) 364 | self.Widget = unreal.FigmaImporterBPLibrary.add_image_widget(self.Parent.FrameAsset, 365 | createInsatanceName(self.Name, self.id), 366 | [ImageTexture2D.blueprint_get_size_x(),ImageTexture2D.blueprint_get_size_y()], # Use the image size rather than stated size to enable LINE to work which reports 0 width 367 | createRelativePosition([self.Parent.xPosition, self.Parent.yPosition], 368 | [self.xPosition, self.yPosition]), 369 | self.ImagePath, self.Alignment, self.MinAnchors, self.MaxAnchors) 370 | saveAsset(self.Parent.FrameAsset) 371 | 372 | # Text class (need to add better font support, currently uses default Inter font) 373 | class FigmaText(): 374 | def __init__(self, Document, TextDict, Parent): 375 | self.Document = Document 376 | self.Name = sanitiseName(TextDict["name"]) 377 | self.Height = TextDict["absoluteBoundingBox"]["height"] 378 | self.Width = TextDict["absoluteBoundingBox"]["width"] 379 | self.xPosition = TextDict["absoluteBoundingBox"]["x"] 380 | self.yPosition = TextDict["absoluteBoundingBox"]["y"] 381 | self.id = TextDict["id"] 382 | self.Parent = Parent 383 | self.Content = TextDict["characters"] 384 | self.FontSize = TextDict["style"]["fontSize"] 385 | self.FontColor = getColorFromFills(TextDict["fills"],{ 386 | "r" : 0, 387 | "g" : 0, 388 | "b" : 0, 389 | "a" : 1, 390 | }) 391 | 392 | self.FontFamily = TextDict["style"]["fontFamily"] 393 | self.MinAnchors = [0,0] 394 | self.MaxAnchors = [0,0] 395 | self.Alignment = [0,0] 396 | 397 | 398 | # Adds text to parent frame 399 | def writeToUnreal(self): 400 | Font = unreal.SlateFontInfo() 401 | Font.set_editor_property('size', self.FontSize) 402 | Font.set_editor_property('FontObject',unreal.EditorAssetLibrary.load_asset("/FigmaImporter/Inter-VariableFont_slnt_wght_Font")) # Uses default Inter font from plugin 403 | Font.set_editor_property('TypefaceFontName', "Inter-VariableFont_slnt_wght") 404 | self.Widget = unreal.FigmaImporterBPLibrary.add_text_widget(self.Parent.FrameAsset, createInsatanceName(self.Name, self.id), self.Content, Font, [self.Width,self.Height], createRelativePosition([self.Parent.xPosition, self.Parent.yPosition], [self.xPosition, self.yPosition]), self.Alignment, self.MinAnchors, self.MaxAnchors) 405 | self.Widget.set_editor_property('color_and_opacity', unreal.SlateColor([self.FontColor["r"],self.FontColor["g"],self.FontColor["b"],self.FontColor["a"]])) 406 | saveAsset(self.Parent.FrameAsset) --------------------------------------------------------------------------------