Repository: TrackMan/Unity.Package.FigmaToUnity Branch: master Commit: 6007b2fd106a Files: 170 Total size: 390.7 KB Directory structure: gitextract_gbxs852s/ ├── .azuredevops/ │ └── pull_request_template.md ├── .gitattributes ├── .gitignore ├── Assets/ │ ├── Panel Settings.asset │ ├── Panel Settings.asset.meta │ ├── UI Theme.tss │ ├── UI Theme.tss.meta │ ├── UnityBase.uss │ └── UnityBase.uss.meta ├── Assets.meta ├── Editor/ │ ├── Assets/ │ │ └── icon.png.meta │ ├── Assets.meta │ ├── Core/ │ │ ├── Api.cs │ │ ├── Api.cs.meta │ │ ├── Assets/ │ │ │ ├── AssetsInfo.cs │ │ │ ├── AssetsInfo.cs.meta │ │ │ ├── CachedAssets.cs │ │ │ ├── CachedAssets.cs.meta │ │ │ ├── GradientWriter.cs │ │ │ ├── GradientWriter.cs.meta │ │ │ ├── ImagesPostprocessor.cs │ │ │ └── ImagesPostprocessor.cs.meta │ │ ├── Assets.meta │ │ ├── JsonUtility.cs │ │ ├── JsonUtility.cs.meta │ │ ├── NodeMetadata.cs │ │ ├── NodeMetadata.cs.meta │ │ ├── NodesRegistry.cs │ │ ├── NodesRegistry.cs.meta │ │ ├── RichText/ │ │ │ ├── RichTextBuilder.cs │ │ │ └── RichTextBuilder.cs.meta │ │ ├── RichText.meta │ │ ├── RootNodes.cs │ │ ├── RootNodes.cs.meta │ │ ├── StylesPreprocessor.cs │ │ ├── StylesPreprocessor.cs.meta │ │ ├── Uss/ │ │ │ ├── BaseUssStyle.cs │ │ │ ├── BaseUssStyle.cs.meta │ │ │ ├── Properties/ │ │ │ │ ├── AssetProperty.cs │ │ │ │ ├── AssetProperty.cs.meta │ │ │ │ ├── ColorProperty.cs │ │ │ │ ├── ColorProperty.cs.meta │ │ │ │ ├── CursorProperty.cs │ │ │ │ ├── CursorProperty.cs.meta │ │ │ │ ├── DurationProperty.cs │ │ │ │ ├── DurationProperty.cs.meta │ │ │ │ ├── EnumProperty.cs │ │ │ │ ├── EnumProperty.cs.meta │ │ │ │ ├── FlexProperty.cs │ │ │ │ ├── FlexProperty.cs.meta │ │ │ │ ├── IntegerProperty.cs │ │ │ │ ├── IntegerProperty.cs.meta │ │ │ │ ├── LayoutDouble4.cs │ │ │ │ ├── LayoutDouble4.cs.meta │ │ │ │ ├── Length2Property.cs │ │ │ │ ├── Length2Property.cs.meta │ │ │ │ ├── Length4Property.cs │ │ │ │ ├── Length4Property.cs.meta │ │ │ │ ├── LengthProperty.cs │ │ │ │ ├── LengthProperty.cs.meta │ │ │ │ ├── NumberProperty.cs │ │ │ │ ├── NumberProperty.cs.meta │ │ │ │ ├── ShadowProperty.cs │ │ │ │ └── ShadowProperty.cs.meta │ │ │ ├── Properties.meta │ │ │ ├── StyleSlot.cs │ │ │ ├── StyleSlot.cs.meta │ │ │ ├── UssStyle.cs │ │ │ ├── UssStyle.cs.meta │ │ │ ├── UssWriter.cs │ │ │ └── UssWriter.cs.meta │ │ ├── Uss.meta │ │ ├── Uxml/ │ │ │ ├── UxmlBuilder.cs │ │ │ ├── UxmlBuilder.cs.meta │ │ │ ├── UxmlWriter.cs │ │ │ └── UxmlWriter.cs.meta │ │ └── Uxml.meta │ ├── Core.meta │ ├── Extensions/ │ │ ├── Extensions.cs │ │ ├── Extensions.cs.meta │ │ ├── NodeExtensions.cs │ │ ├── NodeExtensions.cs.meta │ │ ├── UssStyleExtensions.cs │ │ └── UssStyleExtensions.cs.meta │ ├── Extensions.meta │ ├── Figma.Editor.asmdef │ ├── Figma.Editor.asmdef.meta │ ├── FigmaDownloader.cs │ ├── FigmaDownloader.cs.meta │ ├── FigmaWriter.cs │ ├── FigmaWriter.cs.meta │ ├── Inspector/ │ │ ├── AuthTest.cs │ │ ├── AuthTest.cs.meta │ │ ├── FigmaInspector.cs │ │ ├── FigmaInspector.cs.meta │ │ ├── Styles.cs │ │ └── Styles.cs.meta │ ├── Inspector.meta │ ├── Interface/ │ │ ├── Const.cs │ │ ├── Const.cs.meta │ │ ├── Enums.cs │ │ ├── Enums.cs.meta │ │ ├── Figma.Enums.cs │ │ ├── Figma.Enums.cs.meta │ │ ├── Figma.Types.Interface.cs │ │ ├── Figma.Types.Interface.cs.meta │ │ ├── Figma.Types.Structs.cs │ │ ├── Figma.Types.Structs.cs.meta │ │ ├── Figma.Types.cs │ │ ├── Figma.Types.cs.meta │ │ ├── Interface.Records.cs │ │ └── Interface.Records.cs.meta │ └── Interface.meta ├── Editor.meta ├── License.md ├── License.md.meta ├── Prefabs/ │ ├── Figma.prefab │ └── Figma.prefab.meta ├── Prefabs.meta ├── Readme.md ├── Readme.md.meta ├── Runtime/ │ ├── AssemblyInfo.cs │ ├── AssemblyInfo.cs.meta │ ├── Core/ │ │ ├── Element.cs │ │ ├── Element.cs.meta │ │ ├── QueryAttribute.cs │ │ ├── QueryAttribute.cs.meta │ │ ├── UxmlAttribute.cs │ │ ├── UxmlAttribute.cs.meta │ │ ├── VisualElementMetadata.cs │ │ └── VisualElementMetadata.cs.meta │ ├── Core.meta │ ├── Extensions/ │ │ ├── EnumerableExtensions.cs │ │ ├── EnumerableExtensions.cs.meta │ │ ├── Extensions.cs │ │ ├── Extensions.cs.meta │ │ ├── PathExtensions.cs │ │ ├── PathExtensions.cs.meta │ │ ├── VisualElementExtensions.cs │ │ └── VisualElementExtensions.cs.meta │ ├── Extensions.meta │ ├── Figma.asmdef │ ├── Figma.asmdef.meta │ ├── Figma.cs │ ├── Figma.cs.meta │ ├── Interface/ │ │ ├── Const.cs │ │ ├── Const.cs.meta │ │ ├── Core/ │ │ │ ├── SubElements.cs │ │ │ ├── SubElements.cs.meta │ │ │ ├── SyncElements.cs │ │ │ └── SyncElements.cs.meta │ │ ├── Core.meta │ │ ├── Enums.cs │ │ ├── Enums.cs.meta │ │ ├── Interface.Core.cs │ │ ├── Interface.Core.cs.meta │ │ ├── Interfaces.cs │ │ └── Interfaces.cs.meta │ └── Interface.meta ├── Runtime.meta ├── package.json ├── package.json.meta ├── ~Samples/ │ ├── Scripts/ │ │ ├── Figma.Samples.asmdef │ │ ├── Figma.Samples.asmdef.meta │ │ ├── Test.cs │ │ └── Test.cs.meta │ ├── Scripts.meta │ ├── Test.unity │ └── Test.unity.meta └── ~Samples.meta ================================================ FILE CONTENTS ================================================ ================================================ FILE: .azuredevops/pull_request_template.md ================================================ # ⚠️ Before you merge ⚠️ 1. 👘 Ensure that your code follows the [codestyle](https://dev.azure.com/trackman/Golf/_wiki/wikis/Golf.wiki/35/Code-Style) 2. 🧠 Ensure that you created PRs for ALL branches 3. 🍄 Test your code by finishing one golf round (download fresh course if tools were changed) 4. 🖼️ Submit a screenshot of a finished golf round 5. 💾 Write a [proper](https://keepachangelog.com/en/1.0.0/) description ================================================ FILE: .gitattributes ================================================ # Default * text=auto # Unity *.cs text *.shader text *.hlsl text *.cginc text # Unity YAML *.anim merge=unityyamlmerge auto *.asset merge=unityyamlmerge auto *.controller merge=unityyamlmerge auto *.mat merge=unityyamlmerge auto *.meta merge=unityyamlmerge auto *.physicsMaterial merge=unityyamlmerge auto *.physicsMaterial2D merge=unityyamlmerge auto *.prefab merge=unityyamlmerge auto *.unity merge=unityyamlmerge auto # Unity LFS *.cubemap filter=lfs diff=lfs merge=lfs *.unitypackage filter=lfs diff=lfs merge=lfs # Image *.ai filter=lfs diff=lfs merge=lfs *.apng filter=lfs diff=lfs merge=lfs *.astc filter=lfs diff=lfs merge=lfs *.bmp filter=lfs diff=lfs merge=lfs *.dds filter=lfs diff=lfs merge=lfs *.eps filter=lfs diff=lfs merge=lfs *.exr filter=lfs diff=lfs merge=lfs *.gif filter=lfs diff=lfs merge=lfs *.hdr filter=lfs diff=lfs merge=lfs *.jpeg filter=lfs diff=lfs merge=lfs *.jpg filter=lfs diff=lfs merge=lfs *.JPG filter=lfs diff=lfs merge=lfs *.ktx filter=lfs diff=lfs merge=lfs *.png filter=lfs diff=lfs merge=lfs *.PNG filter=lfs diff=lfs merge=lfs *.psd filter=lfs diff=lfs merge=lfs *.pvr filter=lfs diff=lfs merge=lfs *.svg filter=lfs diff=lfs merge=lfs *.svgz filter=lfs diff=lfs merge=lfs *.tga filter=lfs diff=lfs merge=lfs *.TGA filter=lfs diff=lfs merge=lfs *.tif filter=lfs diff=lfs merge=lfs *.tiff filter=lfs diff=lfs merge=lfs *.webm filter=lfs diff=lfs merge=lfs *.webp filter=lfs diff=lfs merge=lfs # Audio *.aif filter=lfs diff=lfs merge=lfs *.m4a filter=lfs diff=lfs merge=lfs *.mp3 filter=lfs diff=lfs merge=lfs *.ogg filter=lfs diff=lfs merge=lfs *.wav filter=lfs diff=lfs merge=lfs *.WAV filter=lfs diff=lfs merge=lfs # Video *.asf filter=lfs diff=lfs merge=lfs *.avi filter=lfs diff=lfs merge=lfs *.flv filter=lfs diff=lfs merge=lfs *.mov filter=lfs diff=lfs merge=lfs *.mp4 filter=lfs diff=lfs merge=lfs *.mpeg filter=lfs diff=lfs merge=lfs *.mpg filter=lfs diff=lfs merge=lfs *.ogv filter=lfs diff=lfs merge=lfs *.wmv filter=lfs diff=lfs merge=lfs # 3D Object *.blend filter=lfs diff=lfs merge=lfs *.dxf filter=lfs diff=lfs merge=lfs *.fbx filter=lfs diff=lfs merge=lfs *.FBX filter=lfs diff=lfs merge=lfs *.lxo filter=lfs diff=lfs merge=lfs *.ma filter=lfs diff=lfs merge=lfs *.max filter=lfs diff=lfs merge=lfs *.mb filter=lfs diff=lfs merge=lfs *.obj filter=lfs diff=lfs merge=lfs *.raw filter=lfs diff=lfs merge=lfs *.spm filter=lfs diff=lfs merge=lfs *.sbk filter=lfs diff=lfs merge=lfs # Compiled Dynamic Library *.dll filter=lfs diff=lfs merge=lfs *.pdb filter=lfs diff=lfs merge=lfs *.so filter=lfs diff=lfs merge=lfs *.bundle filter=lfs diff=lfs merge=lfs # Compiled Static Library *.a filter=lfs diff=lfs merge=lfs *.la filter=lfs diff=lfs merge=lfs *.lai filter=lfs diff=lfs merge=lfs *.lib filter=lfs diff=lfs merge=lfs *.llblgenproj filter=lfs diff=lfs merge=lfs # Font *.otf filter=lfs diff=lfs merge=lfs *.OTF filter=lfs diff=lfs merge=lfs *.ttf filter=lfs diff=lfs merge=lfs *.TTF filter=lfs diff=lfs merge=lfs *.pdf filter=lfs diff=lfs merge=lfs # CommandLine Utility *.exe filter=lfs diff=lfs merge=lfs # Archives *.zip filter=lfs diff=lfs merge=lfs *.rar filter=lfs diff=lfs merge=lfs *.7z filter=lfs diff=lfs merge=lfs ================================================ FILE: .gitignore ================================================ /[Ll]ibrary/ /[Tt]emp/ /[Ll]ogs/ /[Oo]bj/ /[Bb]uild/ /[Bb]uilds/ /[Uu]ser[Ss]ettings/ /Assets/StreamingAssets* /Assets/UnityEngine* /Assets/Temp* /Packages/com.* /ProjectSettings/Packages* /ProjectSettings/boot.config /ProjectSettings/SceneTemplateSettings.json # Visual Studio cache directory /.vs/ /.vscode/ .vsconfig # Rider cache directory /.idea/ # Autogenerated VS/MD/Consulo solution and project files ExportedObj/ .consulo/ *.csproj *.unityproj *.sln *.suo *.tmp *.user *.userprefs *.pidb *.booproj *.svd *.pdb *.code* # Unity3D generated meta files *.pidb.meta # Unity3D generated packages packages-lock.json # Unity3D generated file on crash reports sysinfo.txt # Builds *.apk *.aab *.unitypackage # OSX files .DS_Store # Generated by analyzers /Assets/Default.ruleset /Assets/Default.ruleset.meta # Pipeline files *.netrc ================================================ FILE: Assets/Panel Settings.asset ================================================ %YAML 1.1 %TAG !u! tag:unity3d.com,2011: --- !u!114 &11400000 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 0} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 19101, guid: 0000000000000000e000000000000000, type: 0} m_Name: Panel Settings m_EditorClassIdentifier: themeUss: {fileID: -4733365628477956816, guid: 999af05a3e7a25744b50c66afc0ac938, type: 3} m_TargetTexture: {fileID: 0} m_ScaleMode: 2 m_ReferenceSpritePixelsPerUnit: 100 m_Scale: 1 m_ReferenceDpi: 96 m_FallbackDpi: 96 m_ReferenceResolution: {x: 1920, y: 1080} m_ScreenMatchMode: 0 m_Match: 0 m_SortingOrder: 0 m_TargetDisplay: 0 m_ClearDepthStencil: 1 m_ClearColor: 0 m_ColorClearValue: {r: 0, g: 0, b: 0, a: 0} m_DynamicAtlasSettings: m_MinAtlasSize: 64 m_MaxAtlasSize: 4096 m_MaxSubTextureSize: 64 m_ActiveFilters: 31 m_AtlasBlitShader: {fileID: 9101, guid: 0000000000000000f000000000000000, type: 0} m_RuntimeShader: {fileID: 9100, guid: 0000000000000000f000000000000000, type: 0} m_RuntimeWorldShader: {fileID: 9102, guid: 0000000000000000f000000000000000, type: 0} textSettings: {fileID: 0} ================================================ FILE: Assets/Panel Settings.asset.meta ================================================ fileFormatVersion: 2 guid: f486055d8ec653edc96c3f3c38380c8f NativeFormatImporter: externalObjects: {} mainObjectFileID: 11400000 userData: assetBundleName: assetBundleVariant: ================================================ FILE: Assets/UI Theme.tss ================================================ @import url("unity-theme://default"); @import url("/Packages/com.trackman.figma/Assets/UnityBase.uss"); VisualElement {} ================================================ FILE: Assets/UI Theme.tss.meta ================================================ fileFormatVersion: 2 guid: 999af05a3e7a25744b50c66afc0ac938 ScriptedImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 2 userData: assetBundleName: assetBundleVariant: script: {fileID: 12388, guid: 0000000000000000e000000000000000, type: 0} disableValidation: 0 ================================================ FILE: Assets/UnityBase.uss ================================================ :root { --selection-color: #5890DE; --cursor-color: #FFFFFF; } .unity-base-field { flex-direction: row; margin: initial; overflow: hidden; flex-shrink: 0; --unity-sync-text-editor-engine: true; } .unity-base-text-field { white-space: nowrap; --unity-selection-color: var(--selection-color); --unity-cursor-color: var(--cursor-color); } .unity-base-field__input { flex: 1 0 0; overflow: hidden; margin: initial; } .unity-base-text-field__input { padding: initial; border-radius: initial; cursor: initial; -unity-overflow-clip-box: content-box; flex: 1 1 auto; background-color: initial; border-color: initial; border-width: initial; margin: initial; --unity-sync-text-editor-engine: true; } .unity-base-text-field__input:focus, .unity-base-text-field__input:hover, .unity-base-text-field:focus > .unity-base-text-field__input, .unity-base-text-field:hover > .unity-base-text-field__input { border-color: var(--selection-color); } .unity-base-slider, .unity-base-slider__dragger, .unity-base-slider__tracker { border-width: initial; border-color: initial; background-color: initial; } .unity-base-slider:focus, .unity-base-slider__dragger, .unity-base-slider__tracker { border-color: initial; } .unity-base-slider--vertical, .unity-base-slider__dragger, .unity-base-slider__tracker { margin: initial; } .unity-scroller, .unity-base-slider__dragger, .unity-base-slider__tracker { border-color: initial; background-color: initial; } .unity-scroller--vertical, .unity-base-slider__dragger, .unity-base-slider__tracker { left: initial; top: initial; margin: initial; border-width: initial; border-color: initial; background-color: initial; } .unity-scroller--vertical > .unity-scroller__slider { margin: initial; width: initial; } ================================================ FILE: Assets/UnityBase.uss.meta ================================================ fileFormatVersion: 2 guid: d97716672acb0ee45bf6697e5adadcdd ScriptedImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 2 userData: assetBundleName: assetBundleVariant: script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} disableValidation: 0 ================================================ FILE: Assets.meta ================================================ fileFormatVersion: 2 guid: 7eedcbb7c4cee534cb8f55eaaa55ee65 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Assets/icon.png.meta ================================================ fileFormatVersion: 2 guid: c0cc7a4d45c57758c894e807bb1af36b TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 13 mipmaps: mipMapMode: 0 enableMipMap: 1 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 flipGreenChannel: 0 isReadable: 0 streamingMipmaps: 0 streamingMipmapsPriority: 0 vTOnly: 0 ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 seamlessCubemap: 0 textureFormat: 1 maxTextureSize: 2048 textureSettings: serializedVersion: 2 filterMode: 1 aniso: 1 mipBias: 0 wrapU: 0 wrapV: 0 wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 spriteMode: 0 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} spritePixelsToUnits: 100 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 1 alphaUsage: 1 alphaIsTransparency: 0 spriteTessellationDetail: -1 textureType: 0 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 ignorePngGamma: 0 applyGammaDecoding: 0 swizzle: 50462976 cookieLightType: 0 platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 64 resizeAlgorithm: 1 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 2048 resizeAlgorithm: 0 textureFormat: -1 textureCompression: 1 compressionQuality: 50 crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] outline: [] physicsShape: [] bones: [] spriteID: internalID: 0 vertices: [] indices: edges: [] weights: [] secondaryTextures: [] nameFileIdTable: {} mipmapLimitGroupName: pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Assets.meta ================================================ fileFormatVersion: 2 guid: 5b1014bd7c33f18afb1f58ad7f928370 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Core/Api.cs ================================================ using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace Figma { using Internals; internal abstract class Api : IDisposable { #region Fields protected readonly string fileKey; protected readonly HttpClient httpClient; #endregion #region Constructors protected Api(string personalAccessToken, string fileKey) { this.fileKey = fileKey; httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("X-FIGMA-TOKEN", personalAccessToken); } #endregion #region Methods void IDisposable.Dispose() => httpClient.Dispose(); #endregion #region Support Methods protected async Task ConvertOnBackgroundAsync(string json, CancellationToken token) where T : class => await Task.Run(() => Task.FromResult(JsonUtility.FromJson(json)), token); protected async Task GetAsync(string get, CancellationToken token = default) where T : class => await ConvertOnBackgroundAsync(await GetJsonAsync(get, token), token); protected async Task GetJsonAsync(string get, CancellationToken token = default) => await HttpGetAsync($"{Internals.Const.api}/{get}", token); async Task HttpGetAsync(string url, CancellationToken token = default) { using HttpRequestMessage request = new(HttpMethod.Get, url); HttpResponseMessage response = await httpClient.SendAsync(request, token); if (response.IsSuccessStatusCode) return await response.Content.ReadAsStringAsync(); throw new HttpRequestException($"{HttpMethod.Get} {url} {response.StatusCode.ToString()}"); } #endregion } } ================================================ FILE: Editor/Core/Api.cs.meta ================================================ fileFormatVersion: 2 guid: 3206e4d9910843d38b90ab361b9a297b timeCreated: 1696830500 ================================================ FILE: Editor/Core/Assets/AssetsInfo.cs ================================================ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using UnityEditor; namespace Figma.Core.Assets { using Internals; using static Internals.Const; internal class AssetsInfo { #region Fields internal readonly string directory; internal readonly string relativeDirectory; internal readonly CachedAssets cachedAssets; internal readonly ConcurrentBag modifiedContent; readonly IReadOnlyList fontDirectories; #endregion #region Constructors internal AssetsInfo(string directory, string relativeDirectory, string remapsFileName, IReadOnlyList fontDirectories) { this.directory = directory; this.relativeDirectory = relativeDirectory; this.fontDirectories = fontDirectories; modifiedContent = new ConcurrentBag(); cachedAssets = new CachedAssets(directory, remapsFileName); } #endregion #region Methods internal bool GetAssetPath(string name, string extension, out string path) { switch (extension) { case KnownFormats.otf or KnownFormats.ttf: path = GetFontPath(name, extension); return path.NotNullOrEmpty(); case KnownFormats.asset: string fontAssetPath = GetFontPath(name, extension); string fontDirectoryPath = Path.GetDirectoryName(fontAssetPath); string file = $"{name} SDF.{extension}"; path = fontDirectoryPath.NotNullOrEmpty() ? file : PathExtensions.CombinePath(fontDirectoryPath, file); return fontAssetPath.NotNullOrEmpty(); case KnownFormats.png or KnownFormats.svg: string mappedName = cachedAssets[name]; path = PathExtensions.CombinePath(imagesDirectoryName, $"{mappedName}.{extension}"); return File.Exists(PathExtensions.CombinePath(directory, path)); default: throw new NotSupportedException(extension); } } internal void AddModifiedFiles(params string[] items) => items.ForEach(item => modifiedContent.Add(item)); internal string GetAbsolutePath(string path) => PathExtensions.CombinePath(directory, path); #endregion #region Support Methods string GetFontPath(string name, string extension) { string file = $"{name}.{extension}"; string localFontsPath = PathExtensions.CombinePath(fontsDirectoryName, file); string relativePath = PathExtensions.CombinePath(relativeDirectory, localFontsPath); if (File.Exists(FileUtil.GetPhysicalPath(relativePath))) return PathExtensions.unixPathSeperator + relativePath; foreach (string fontsDirectory in fontDirectories) { string projectFontPath = PathExtensions.CombinePath(fontsDirectory, file); if (File.Exists(FileUtil.GetPhysicalPath(projectFontPath))) return PathExtensions.unixPathSeperator + projectFontPath; } return null; } #endregion } } ================================================ FILE: Editor/Core/Assets/AssetsInfo.cs.meta ================================================ fileFormatVersion: 2 guid: 45f0ac71191d448fb945cd8b96c71dd0 timeCreated: 1733916355 ================================================ FILE: Editor/Core/Assets/CachedAssets.cs ================================================ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Figma.Core.Assets { using Internals; using static Internals.PathExtensions; internal sealed class CachedAssets { #region Fields readonly string targetFilePath; #endregion #region Constructor internal CachedAssets(string directory, string name) => targetFilePath = CombinePath(directory, $"{nameof(CachedAssets)}-{name}.{KnownFormats.json}"); #endregion #region Properties internal Dictionary Map { get; private set; } #endregion #region Operators internal string this[string key] { get => Map.GetValueOrDefault(key, key); set => Map[key] = value; } #endregion #region Methods internal async Task LoadAsync(CancellationToken token) => Map = File.Exists(targetFilePath) ? JsonUtility.FromJson>(await File.ReadAllTextAsync(targetFilePath, token)) : new Dictionary(); internal async Task SaveAsync() => await File.WriteAllTextAsync(targetFilePath, JsonUtility.ToJson(Map, prettyPrint: true)); #endregion } } ================================================ FILE: Editor/Core/Assets/CachedAssets.cs.meta ================================================ fileFormatVersion: 2 guid: 17a02e0c1cba42429bc345ea5794a854 timeCreated: 1733918400 ================================================ FILE: Editor/Core/Assets/GradientWriter.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using System.Xml; using UnityEngine; namespace Figma.Core.Assets { using Internals; using static Const; internal class GradientWriter : IDisposable { #region Fields static readonly XmlWriterSettings xmlWriterSettings = new() { Indent = true, NewLineOnAttributes = true, NewLineChars = Environment.NewLine, IndentChars = indentCharacters, Async = true }; readonly XmlWriter writer; #endregion #region Constructors public GradientWriter(string xmlPath) => writer = XmlWriter.Create(xmlPath, xmlWriterSettings); #endregion #region Methods public async Task WriteAsync(GradientPaint gradient, CancellationToken token) { writer.WriteStartElement(KnownFormats.svg); writer.WriteStartElement("defs"); switch (gradient.type) { case PaintType.GRADIENT_LINEAR: writer.WriteStartElement("linearGradient"); writer.WriteAttributeString("id", nameof(gradient)); for (int i = 0; i < Mathf.Max(gradient.gradientHandlePositions.Length, 2); ++i) { writer.WriteAttributeString($"x{i + 1}", gradient.gradientHandlePositions[i].x.ToString("F2", Culture)); writer.WriteAttributeString($"y{i + 1}", gradient.gradientHandlePositions[i].y.ToString("F2", Culture)); } break; case PaintType.GRADIENT_RADIAL: case PaintType.GRADIENT_DIAMOND: writer.WriteStartElement("radialGradient"); writer.WriteAttributeString("id", nameof(gradient)); writer.WriteAttributeString("fx", gradient.gradientHandlePositions[0].x.ToString("F2", Culture)); writer.WriteAttributeString("fy", gradient.gradientHandlePositions[0].y.ToString("F2", Culture)); writer.WriteAttributeString("cx", gradient.gradientHandlePositions[0].x.ToString("F2", Culture)); writer.WriteAttributeString("cy", gradient.gradientHandlePositions[0].y.ToString("F2", Culture)); Vector2 a = new ((float)gradient.gradientHandlePositions[1].x, (float)gradient.gradientHandlePositions[1].y); Vector2 b = new ((float)gradient.gradientHandlePositions[0].x, (float)gradient.gradientHandlePositions[0].y); float radius = (a - b).magnitude; writer.WriteAttributeString("r", radius.ToString("F2", Culture)); break; default: throw new NotSupportedException(); } foreach (ColorStop stop in gradient.gradientStops) { writer.WriteStartElement(nameof(stop)); writer.WriteAttributeString("offset", stop.position.ToString("F2", Culture)); writer.WriteAttributeString("style", $"stop-color:rgb({(byte)(stop.color.r * 255)},{(byte)(stop.color.g * 255)},{(byte)(stop.color.b * 255)});stop-opacity:{stop.color.a.ToString("F2", Culture)}"); await writer.WriteEndElementAsync(); } await writer.WriteEndElementAsync(); token.ThrowIfCancellationRequested(); await writer.WriteEndElementAsync(); token.ThrowIfCancellationRequested(); writer.WriteStartElement("rect"); writer.WriteAttributeString("width", "100"); writer.WriteAttributeString("height", "100"); writer.WriteAttributeString("fill", "url(#gradient)"); if (gradient.opacity < 1.0) writer.WriteAttributeString("fill-opacity", gradient.opacity.ToString("F2", Culture)); await writer.WriteEndElementAsync(); token.ThrowIfCancellationRequested(); await writer.WriteEndElementAsync(); token.ThrowIfCancellationRequested(); } public void Dispose() => writer?.Close(); #endregion } } ================================================ FILE: Editor/Core/Assets/GradientWriter.cs.meta ================================================ fileFormatVersion: 2 guid: ec2d941c5d164f0b8887c69795bd3767 timeCreated: 1734429409 ================================================ FILE: Editor/Core/Assets/ImagesPostprocessor.cs ================================================ using Unity.VectorGraphics.Editor; using UnityEditor; #pragma warning disable S1144 // Called from Unity namespace Figma.Core.Assets { internal class ImagesPostprocessor : AssetPostprocessor { #region Methods void OnPreprocessAsset() { if (!assetPath.Contains("UI/Assets/Images")) return; if (assetImporter is SVGImporter svgImporter) #if UNITY_6000_3_OR_NEWER svgImporter.SvgType = SVGType.UISVGImage; #else svgImporter.SvgType = SVGType.UIToolkit; #endif if (assetImporter is not TextureImporter textureImporter) return; textureImporter.npotScale = TextureImporterNPOTScale.None; textureImporter.mipmapEnabled = false; TextureImporterPlatformSettings androidOverrides = textureImporter.GetPlatformTextureSettings("Android"); androidOverrides.overridden = true; androidOverrides.format = TextureImporterFormat.ETC2_RGBA8Crunched; androidOverrides.compressionQuality = 90; textureImporter.SetPlatformTextureSettings(androidOverrides); } #endregion } } ================================================ FILE: Editor/Core/Assets/ImagesPostprocessor.cs.meta ================================================ fileFormatVersion: 2 guid: 830782a47c09eebd4b66c78e8b91c6a7 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Core/Assets.meta ================================================ fileFormatVersion: 2 guid: 569bf8b4580a4bd792f747a7a503034c timeCreated: 1733918411 ================================================ FILE: Editor/Core/JsonUtility.cs ================================================ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Buffers; using System.IO; using UnityEngine; namespace Figma.Internals { public class JsonUtility { class ArrayPool : IArrayPool { #region Methods public char[] Rent(int minimumLength) => ArrayPool.Shared.Rent(minimumLength); public void Return(char[] array) => ArrayPool.Shared.Return(array); #endregion } #region Properties static JsonSerializer serializer = new(); static readonly IArrayPool arrayPool = new ArrayPool(); #endregion #region Constructors #if UNITY_EDITOR [UnityEditor.InitializeOnLoadMethod] #endif [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] public static void Initialize() { JsonSerializerSettings settings = new() { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore, Converters = { new EffectArrayConverter(), new PaintArrayConverter(), new LayoutGridArrayConverter(), new ExportSettingsArrayConverter(), new TransitionConverter(), new BaseNodeArrayConverter(), new SceneNodeArrayConverter() } }; serializer = JsonSerializer.Create(settings); } public static string ToJson(T value, bool prettyPrint) { using StringWriter stringWriter = new(); using JsonTextWriter jsonTextWriter = new(stringWriter) { Formatting = prettyPrint ? Formatting.Indented : Formatting.None }; serializer.Serialize(jsonTextWriter, value); return stringWriter.ToString(); } public static T FromJson(string json, bool useArrayPool = true) { using StringReader stringReader = new(json); using JsonTextReader jsonTextReader = new(stringReader); if (useArrayPool) jsonTextReader.ArrayPool = arrayPool; return serializer.Deserialize(jsonTextReader); } #endregion } public abstract class ArrayConverter : JsonConverter { #region Methods public override bool CanConvert(Type objectType) => objectType == typeof(T[]); public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { JArray array = JArray.Load(reader); T[] result = new T[array.Count]; for (int i = 0; i < array.Count; ++i) result[i] = ToObject((JObject)array[i], serializer); return result; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { T[] array = (T[])value; writer.WriteStartArray(); foreach (T node in array) serializer.Serialize(writer, node); writer.WriteEndArray(); } protected TEnum GetValue(JObject obj, string name = "type") => (TEnum)Enum.Parse(typeof(TEnum), obj[name].Value()); protected abstract T ToObject(JObject obj, JsonSerializer serializer); #endregion } public class EffectArrayConverter : ArrayConverter { #region Methods protected override Effect ToObject(JObject obj, JsonSerializer serializer) => GetValue(obj) switch { EffectType.INNER_SHADOW => obj.ToObject(serializer), EffectType.DROP_SHADOW => obj.ToObject(serializer), EffectType.LAYER_BLUR => obj.ToObject(serializer), EffectType.BACKGROUND_BLUR => obj.ToObject(serializer), _ => throw new NotSupportedException() }; #endregion } public class PaintArrayConverter : ArrayConverter { #region Methods protected override Paint ToObject(JObject obj, JsonSerializer serializer) => GetValue(obj) switch { PaintType.SOLID => obj.ToObject(serializer), PaintType.GRADIENT_LINEAR => obj.ToObject(serializer), PaintType.GRADIENT_RADIAL => obj.ToObject(serializer), PaintType.GRADIENT_ANGULAR => obj.ToObject(serializer), PaintType.GRADIENT_DIAMOND => obj.ToObject(serializer), PaintType.IMAGE => obj.ToObject(serializer), PaintType.EMOJI => obj.ToObject(serializer), _ => throw new NotSupportedException() }; #endregion } public class LayoutGridArrayConverter : ArrayConverter { #region Methods protected override LayoutGrid ToObject(JObject obj, JsonSerializer serializer) => GetValue(obj, "pattern") switch { Pattern.COLUMNS => obj.ToObject(serializer), Pattern.ROWS => obj.ToObject(serializer), Pattern.GRID => obj.ToObject(serializer), _ => throw new NotSupportedException() }; #endregion } public class ExportSettingsArrayConverter : ArrayConverter { #region Methods protected override ExportSettings ToObject(JObject obj, JsonSerializer serializer) => GetValue(obj, "format") switch { Format.JPG => obj.ToObject(serializer), Format.PNG => obj.ToObject(serializer), Format.SVG => obj.ToObject(serializer), Format.PDF => obj.ToObject(serializer), _ => throw new NotSupportedException() }; #endregion } public class TransitionConverter : JsonConverter { #region Methods public override bool CanConvert(Type objectType) => objectType == typeof(Transition); public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { JObject obj = JObject.Load(reader); return (TransitionType)Enum.Parse(typeof(TransitionType), obj["type"]!.Value()) switch { TransitionType.DISSOLVE => obj.ToObject(serializer), TransitionType.SMART_ANIMATE => obj.ToObject(serializer), TransitionType.MOVE_IN => obj.ToObject(serializer), TransitionType.MOVE_OUT => obj.ToObject(serializer), TransitionType.PUSH => obj.ToObject(serializer), TransitionType.SLIDE_IN => obj.ToObject(serializer), TransitionType.SLIDE_OUT => obj.ToObject(serializer), _ => throw new NotSupportedException() }; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); #endregion } public class BaseNodeArrayConverter : ArrayConverter { #region Methods protected override BaseNode ToObject(JObject obj, JsonSerializer serializer) => GetValue(obj) switch { NodeType.DOCUMENT => obj.ToObject(serializer), NodeType.CANVAS => obj.ToObject(serializer), _ => throw new NotSupportedException() }; #endregion } public class SceneNodeArrayConverter : ArrayConverter { #region Methods protected override SceneNode ToObject(JObject obj, JsonSerializer serializer) => GetValue(obj) switch { NodeType.SLICE => obj.ToObject(serializer), NodeType.FRAME => obj.ToObject(serializer), NodeType.GROUP => obj.ToObject(serializer), NodeType.COMPONENT_SET => obj.ToObject(serializer), NodeType.COMPONENT => obj.ToObject(serializer), NodeType.INSTANCE => obj.ToObject(serializer), NodeType.BOOLEAN_OPERATION => obj.ToObject(serializer), NodeType.VECTOR => obj.ToObject(serializer), NodeType.STAR => obj.ToObject(serializer), NodeType.LINE => obj.ToObject(serializer), NodeType.ELLIPSE => obj.ToObject(serializer), NodeType.REGULAR_POLYGON => obj.ToObject(serializer), NodeType.RECTANGLE => obj.ToObject(serializer), NodeType.TEXT => obj.ToObject(serializer), NodeType.SECTION => obj.ToObject(serializer), _ => throw new NotSupportedException() }; #endregion } public class FigmaGeneration { } } ================================================ FILE: Editor/Core/JsonUtility.cs.meta ================================================ fileFormatVersion: 2 guid: c03fe5a0d0b4de848ae40e0ab3bfaac6 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Core/NodeMetadata.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; namespace Figma { using Attributes; using Internals; using static Internals.PathExtensions; internal class NodeMetadata { #region Consts static readonly Dictionary typeMap = new() { // Base elements { typeof(VisualElement), ElementType.VisualElement }, { typeof(BindableElement), ElementType.BindableElement }, // Utilities { typeof(Box), ElementType.Box }, { typeof(TextElement), ElementType.TextElement }, { typeof(Label), ElementType.Label }, { typeof(Image), ElementType.Image }, { typeof(IMGUIContainer), ElementType.IMGUIContainer }, { typeof(Foldout), ElementType.Foldout }, // Controls { typeof(Button), ElementType.Button }, { typeof(RepeatButton), ElementType.RepeatButton }, { typeof(Toggle), ElementType.Toggle }, { typeof(Scroller), ElementType.Scroller }, { typeof(Slider), ElementType.Slider }, { typeof(SliderInt), ElementType.SliderInt }, { typeof(MinMaxSlider), ElementType.MinMaxSlider }, { typeof(EnumField), ElementType.EnumField }, { typeof(MaskField), ElementType.MaskField }, { typeof(LayerField), ElementType.LayerField }, { typeof(LayerMaskField), ElementType.LayerMaskField }, { typeof(TagField), ElementType.TagField }, { typeof(ProgressBar), ElementType.ProgressBar }, // Text input { typeof(TextField), ElementType.TextField }, { typeof(IntegerField), ElementType.IntegerField }, { typeof(LongField), ElementType.LongField }, { typeof(FloatField), ElementType.FloatField }, { typeof(DoubleField), ElementType.DoubleField }, { typeof(Vector2Field), ElementType.Vector2Field }, { typeof(Vector2IntField), ElementType.Vector2IntField }, { typeof(Vector3Field), ElementType.Vector3Field }, { typeof(Vector3IntField), ElementType.Vector3IntField }, { typeof(Vector4Field), ElementType.Vector4Field }, { typeof(RectField), ElementType.RectField }, { typeof(RectIntField), ElementType.RectIntField }, { typeof(BoundsField), ElementType.BoundsField }, { typeof(BoundsIntField), ElementType.BoundsIntField }, // Complex widgets { typeof(PropertyField), ElementType.PropertyField }, { typeof(ColorField), ElementType.ColorField }, { typeof(CurveField), ElementType.CurveField }, { typeof(GradientField), ElementType.GradientField }, { typeof(ObjectField), ElementType.ObjectField }, // Toolbar { typeof(Toolbar), ElementType.Toolbar }, { typeof(ToolbarButton), ElementType.ToolbarButton }, { typeof(ToolbarToggle), ElementType.ToolbarToggle }, { typeof(ToolbarMenu), ElementType.ToolbarMenu }, { typeof(ToolbarSearchField), ElementType.ToolbarSearchField }, { typeof(ToolbarPopupSearchField), ElementType.ToolbarPopupSearchField }, { typeof(ToolbarSpacer), ElementType.ToolbarSpacer }, // Views and windows { typeof(ListView), ElementType.ListView }, { typeof(ScrollView), ElementType.ScrollView }, { typeof(PopupWindow), ElementType.PopupWindow } }; #endregion #region Fields readonly Dictionary rootMetadata = new(); readonly Dictionary queryMetadata = new(); readonly List search = new(256); #endregion #region Properties static BindingFlags FieldsFlags => BindingFlags.NonPublic | BindingFlags.Instance; #endregion #region Constructors internal NodeMetadata(DocumentNode documentNode, IEnumerable elements, bool filter, bool throwExceptions = true, bool silent = false) { void InitializeRootElement(Type elementType) { void InitializeElement(Type type, IBaseNodeMixin rootNode) { IBaseNodeMixin FindNodeByQuery(QueryAttribute queryRoot, QueryAttribute query, bool throwException) => queryRoot != null && !ReferenceEquals(queryRoot, query) && Find(rootNode, queryRoot.Path, throwException, silent) is { } queryRootNode ? Find(queryRootNode, query.Path, throwException, silent) : Find(rootNode, query.Path, throwException, silent); QueryAttribute queryRoot = null; foreach (FieldInfo field in type.GetFields(FieldsFlags)) { Type fieldType = field.FieldType; QueryAttribute query = field.GetCustomAttribute(); if (query is null) continue; if (query.StartRoot) queryRoot = query; IBaseNodeMixin node = FindNodeByQuery(queryRoot, query, throwExceptions && !query.Nullable && query.ReplaceElementPath.NullOrEmpty() && query.RebuildElementEvent.NullOrEmpty()); if (node != null && !queryMetadata.ContainsKey(node)) queryMetadata.Add(node, new QueryMetadata(fieldType, query)); if (query.EndRoot) queryRoot = null; if (node != null && typeof(ISubElement).IsAssignableFrom(fieldType)) InitializeElement(fieldType, node); } } UxmlAttribute uxml = elementType.GetCustomAttribute(); if (uxml is null) return; IBaseNodeMixin elementRoot = Find(documentNode, uxml.Root); IBaseNodeMixin[] elementPreserve = uxml.Preserve.Select(x => Find(documentNode, x)).ToArray(); rootMetadata.Add(elementRoot, new RootMetadata(filter, uxml, uxml.DownloadImages)); foreach (IBaseNodeMixin value in elementPreserve.Where(x => !rootMetadata.ContainsKey(x))) rootMetadata.Add(value, new RootMetadata(filter, uxml, UxmlDownloadImages.Everything)); InitializeElement(elementType, elementRoot); } elements.ForEach(InitializeRootElement); } #endregion #region Methods internal bool EnabledInHierarchy(IBaseNodeMixin node) => !rootMetadata.Any(x => x.Value.filter) || GetMetadata(node).root != null; internal bool ShouldDownload(IBaseNodeMixin node, UxmlDownloadImages flag) { BaseNodeMetadata metadata = GetMetadata(node); if (metadata.root is null || !metadata.root.filter) return true; bool shouldDownload = metadata.root.downloadImages == UxmlDownloadImages.Everything || metadata.root.downloadImages.HasFlag(flag); if (!metadata.root.downloadImages.HasFlag(UxmlDownloadImages.ByElements) || metadata.query is null) return shouldDownload; return metadata.query.query.DownloadImage switch { ElementDownloadImage.Download => true, ElementDownloadImage.Ignore => false, _ => shouldDownload }; } internal (bool isHash, string templateName) GetTemplate(IBaseNodeMixin node) { string GetFullPath(IBaseNodeMixin x) => x.parent != null ? CombinePath(GetFullPath(x.parent), x.name) : x.name; BaseNodeMetadata metadata = GetMetadata(node); if (metadata.root is null || !metadata.root.filter || metadata.query is null) return (false, null); return !metadata.query.query.Hash ? (false, metadata.query.query.Template) : (true, $"{metadata.query.fieldType.Name}-{Hash128.Compute(GetFullPath(node))}"); } internal (ElementType, string) GetElementType(IBaseNodeMixin node) { ElementType FieldTypeToElementType(Type type) => typeMap.TryGetValue(type, out ElementType elementType) ? elementType : typeof(VisualElement).IsAssignableFrom(type) ? ElementType.IElement : throw new ArgumentOutOfRangeException(type.FullName); BaseNodeMetadata metadata = GetMetadata(node); return metadata.root != null && metadata.root.filter && metadata.root.uxml.TypeIdentification == UxmlElementTypeIdentification.ByElementType && metadata.query != null ? (FieldTypeToElementType(metadata.query.fieldType), metadata.query.fieldType!.FullName!.Replace("+", ".")) : (ElementType.None, null); } #endregion #region Support Methods IBaseNodeMixin Find(IBaseNodeMixin value, string path, bool throwException = true, bool silent = false) { IEnumerable Search(IBaseNodeMixin value, string path) { bool StartsWith(string path, IBaseNodeMixin value, int startIndex) { int endIndex = startIndex + value.name.Length; return path.BeginsWith(value.name, startIndex) && path.Length >= endIndex && (path.Length == endIndex || path[endIndex].IsSeparator()); } int LastIndexOf(IBaseNodeMixin root, IBaseNodeMixin leaf, IBaseNodeMixin value, string path, int startIndex = 0) { if (value.parent != null && value.parent != root) startIndex = LastIndexOf(root, leaf, value.parent, path, startIndex); if (startIndex < 0 || !StartsWith(path, value, startIndex)) return -1; int endIndex = startIndex + value.name.Length; if (path.Length > endIndex && path[endIndex].IsSeparator() && value != leaf) endIndex++; return endIndex; } void SearchIn(IBaseNodeMixin value, string path, int startIndex = 0) { static bool IsVisible(IBaseNodeMixin mixin) { if (mixin is ISceneNodeMixin { visible: false }) return false; return mixin.parent is null || IsVisible(mixin.parent); } static IReadOnlyCollection GetChildren(IBaseNodeMixin value) { List children = new(); switch (value) { case DocumentNode documentNode: children.AddRange(documentNode.children); break; case IChildrenMixin childrenMixin: children.AddRange(childrenMixin.children); break; } return children; } static bool EqualsTo(IBaseNodeMixin value, string path, int startIndex) => path.EqualsTo(value.name, startIndex); IReadOnlyCollection children = GetChildren(value); search.AddRange(children.Where(child => IsVisible(child) && child.name.NotNullOrEmpty() && EqualsTo(child, path, startIndex))); children.Where(child => IsVisible(child) && child.name.NotNullOrEmpty() && StartsWith(path, child, startIndex)).ForEach(child => SearchIn(child, path, startIndex + child.name.Length + 1)); } void SearchByFullPath(IBaseNodeMixin value, string path, int startIndex = 0) { static bool IsVisible(IBaseNodeMixin mixin) { if (mixin is ISceneNodeMixin { visible: false }) return false; return mixin.parent is null || IsVisible(mixin.parent); } static IReadOnlyCollection GetChildren(IBaseNodeMixin value) { List children = new(); switch (value) { case DocumentNode documentNode: children.AddRange(documentNode.children); break; case IChildrenMixin childrenMixin: children.AddRange(childrenMixin.children); break; } return children; } IReadOnlyCollection children = GetChildren(value); bool EqualsToFullPath(IBaseNodeMixin root, IBaseNodeMixin value, string path, int startIndex) => LastIndexOf(root, value, value, path, startIndex) == path.Length; bool StartsWithFullPath(IBaseNodeMixin root, IBaseNodeMixin value, string path, int startIndex) { int endIndex = LastIndexOf(root, value, value, path, startIndex); return endIndex >= 0 && path.Length > endIndex && path[endIndex].IsSeparator(); } search.AddRange(children.Where(child => IsVisible(child) && child.name.NotNullOrEmpty() && EqualsToFullPath(value, child, path, startIndex))); foreach (IBaseNodeMixin child in children.Where(child => IsVisible(child) && child.name.NotNullOrEmpty() && StartsWithFullPath(value, child, path, startIndex))) SearchByFullPath(child, path, startIndex + child.name.Length + 1); } search.Clear(); IBaseNodeMixin root = FindRoot(value); if (root != null) { UxmlAttribute uxml = rootMetadata[root].uxml; if (path.BeginsWith(uxml.DocumentRoot) || uxml.DocumentPreserve.Any(x => path.BeginsWith(x))) SearchByFullPath(root.parent.parent, path, UxmlAttribute.prefix.Length + 1); else SearchIn(value, path); } else SearchByFullPath(value, path); return search; } IBaseNodeMixin result = Search(value, path).FirstOrDefault(); if (result != null) return result; if (throwException) throw new Exception(Internals.Extensions.BuildTargetMessage("Cannot find node at", CombinePath(value.GetFullPath(), path))); if (!silent) Debug.LogWarning(Internals.Extensions.BuildTargetMessage("Cannot find node at", CombinePath(value.GetFullPath(), path))); return null; } IBaseNodeMixin FindRoot(IBaseNodeMixin value) { try { while (value != null) { if (rootMetadata.ContainsKey(value)) return value; value = value.parent; } return null; } catch (Exception exception) { Debug.LogWarning(exception); throw; } } BaseNodeMetadata GetMetadata(IBaseNodeMixin value) { IBaseNodeMixin FindRootInChildren(IBaseNodeMixin value) { if (rootMetadata.ContainsKey(value)) return value; switch (value) { case DocumentNode documentNode: foreach (CanvasNode child in documentNode.children) { IBaseNodeMixin node = FindRootInChildren(child); if (node != null) return node; } break; case IChildrenMixin children: foreach (SceneNode child in children.children) { IBaseNodeMixin node = FindRootInChildren(child); if (node != null) return node; } break; } return null; } IBaseNodeMixin root = FindRoot(value) ?? FindRootInChildren(value); return root != null ? new BaseNodeMetadata(rootMetadata[root], queryMetadata.GetValueOrDefault(value)) : new BaseNodeMetadata(null, null); } #endregion } } ================================================ FILE: Editor/Core/NodeMetadata.cs.meta ================================================ fileFormatVersion: 2 guid: 826eb9624b2845ffafc480902c5ed5d6 timeCreated: 1696228041 ================================================ FILE: Editor/Core/NodesRegistry.cs ================================================ using System.Collections.Generic; using System.Linq; namespace Figma.Core { using Internals; using Const = Const; internal sealed class NodesRegistry { #region Fields internal List MissingComponents { get; } = new(Const.initialCollectionCapacity); internal List ImageFills { get; } = new(Const.initialCollectionCapacity); internal List Pngs { get; } = new(Const.initialCollectionCapacity); internal List Svgs { get; } = new(Const.initialCollectionCapacity); internal Dictionary Gradients { get; } = new(Const.initialCollectionCapacity); #endregion public NodesRegistry(Data data, NodeMetadata nodeMetadata) { List nodes = data.document.children.SelectMany(canvas => canvas.Flatten(node => node.IsVisible() && nodeMetadata.EnabledInHierarchy(node) && node.parent is not BooleanOperationNode)).ToList(); MissingComponents.AddRange(nodes.OfType() .Where(instance => data.document.Flatten().Any(node => node.id == instance.componentId)) .Select(instance => instance.componentId)); Pngs.AddRange(nodes.Where(node => node is not BooleanOperationNode && node.IsSvgNode() && node.HasImage())); Svgs.AddRange(nodes.Where(node => node.IsSvgNode() && !node.HasImage())); ImageFills.AddRange(nodes.Where(node => node is not BooleanOperationNode && !node.IsSvgNode() && node.HasImage())); foreach (GradientPaint gradient in nodes.OfType() .Where(x => x is not BooleanOperationNode) .SelectMany(x => x.fills.OfType())) Gradients.TryAdd(gradient.GetHash(), gradient); } } } ================================================ FILE: Editor/Core/NodesRegistry.cs.meta ================================================ fileFormatVersion: 2 guid: e6f8d15f8d194578ad29c8c0a4a0d2e6 timeCreated: 1734424985 ================================================ FILE: Editor/Core/RichText/RichTextBuilder.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; namespace Figma.Core.RichText { using Internals; internal sealed class TextBuilder { #region Container enum TagType { Bold, Italic, Underline, Strikethrough, Color, FontSize, FontWeight, Indent, } class Tag { #region Fields readonly string tag; readonly StringBuilder stringBuilder; string value; bool active; #endregion #region Constructor public Tag(StringBuilder stringBuilder, string tag) { this.tag = tag; this.stringBuilder = stringBuilder; } #endregion #region Methods public void Set(bool required) { if (!active && required) Open(); if (active && !required) Close(); if (!required) value = null; } public void Set(bool required, string value) { switch (required) { case true when active && this.value != value && this.value != null: Close(); this.value = value; Open(); return; case false when !active: return; default: this.value = value; Set(required); break; } } void Open() { stringBuilder.Append(string.IsNullOrEmpty(value) ? $"<{tag}>" : $"<{tag}={value}>"); active = true; } void Close() { stringBuilder.Append($""); active = false; } #endregion } #endregion #region Fields readonly StringBuilder stringBuilder = new(); readonly Dictionary tags; readonly TextNode node; #endregion #region Constructors public TextBuilder(TextNode textNode) { tags = new Dictionary { { TagType.Bold, new Tag(stringBuilder, "b") }, { TagType.Italic, new Tag(stringBuilder, "i") }, { TagType.Underline, new Tag(stringBuilder, "u") }, { TagType.Strikethrough, new Tag(stringBuilder, "strikethrough") }, { TagType.Color, new Tag(stringBuilder, "color") }, { TagType.FontSize, new Tag(stringBuilder, "size") }, { TagType.FontWeight, new Tag(stringBuilder, "font-weight") }, { TagType.Indent, new Tag(stringBuilder, "indent") }, }; node = textNode; } #endregion #region Methods public string Build() { string text = node.characters; TextNode.Style baseStyle = node.style; int[] charOverrides = node.characterStyleOverrides; Dictionary styleTable = node.styleOverrideTable; LineType[] lineTypes = node.lineTypes; int[] lineIndents = node.lineIndentations ?? Array.Empty(); int textLength = text.Length; if (charOverrides.Length < textLength) { Array.Resize(ref charOverrides, textLength); for (int j = node.characterStyleOverrides.Length; j < textLength; j++) charOverrides[j] = 0; } int listLineIndex = 0; for (int i = 0, line = 0; i < textLength; i++) { char ch = text[i]; if (i == 0 || text[i - 1] == '\n') { if (i == 0) line = 0; else line++; int indentLevel = line < lineIndents.Length ? lineIndents[line] : 0; LineType lineType = line < lineTypes.Length ? lineTypes[line] : LineType.NONE; for (int s = 0; s < indentLevel; s++) tags[TagType.Indent].Set(lineType != LineType.NONE, (10 * indentLevel).ToString()); if (lineType is LineType.UNORDERED or LineType.NONE) listLineIndex = 0; if (lineType is LineType.ORDERED) stringBuilder.Append($"{++listLineIndex}. "); else if (lineType is LineType.UNORDERED) stringBuilder.Append("• "); } if (ch == '\n') { foreach (Tag tag in tags.Values) tag.Set(false); stringBuilder.Append('\n'); continue; } int styleOverrideId = i < charOverrides.Length ? charOverrides[i] : 0; TextNode.Style charStyle = styleTable.GetValueOrDefault(styleOverrideId, baseStyle); if (charStyle != null) { tags[TagType.Bold].Set(charStyle.fontWeight >= (int)FontWeight.Bold); tags[TagType.Italic].Set(charStyle.italic); tags[TagType.Underline].Set(charStyle.textDecoration is TextDecoration.UNDERLINE); tags[TagType.Strikethrough].Set(charStyle.textDecoration is TextDecoration.STRIKETHROUGH); SolidPaint paint = charStyle.fills?.OfType().FirstOrDefault(); tags[TagType.Color].Set(paint != null, paint == null ? null : "#" + ColorUtility.ToHtmlStringRGBA((Color)paint.color)); tags[TagType.FontWeight].Set(charStyle.fontWeight != (double)FontWeight.Regular, charStyle.fontWeight.ToString()); tags[TagType.FontSize].Set(true, charStyle.fontSize.ToString()); } stringBuilder.Append(ch); } foreach (Tag tag in tags.Values) tag.Set(false); return stringBuilder.ToString(); } #endregion } } ================================================ FILE: Editor/Core/RichText/RichTextBuilder.cs.meta ================================================ fileFormatVersion: 2 guid: 63d2b4dce6024b989f78957756f9f22f timeCreated: 1743683644 ================================================ FILE: Editor/Core/RichText.meta ================================================ fileFormatVersion: 2 guid: b4773ce3ee3d4f9f817a7dca740b3afc timeCreated: 1743683652 ================================================ FILE: Editor/Core/RootNodes.cs ================================================ using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Figma.Core { using Internals; using Attributes; internal class RootNodes { const int initialCollectionCapacity = 32; #region Fields readonly List canvases = new(initialCollectionCapacity); readonly List componentSets = new(initialCollectionCapacity); readonly List frames = new(initialCollectionCapacity); readonly List<(DefaultShapeNode, string hash)> elements = new(initialCollectionCapacity); #endregion #region Properties public IReadOnlyList Canvases => canvases; public IReadOnlyList ComponentSets => componentSets; public IReadOnlyList Frames => frames; public IReadOnlyList<(DefaultShapeNode node, string hash)> Elements => elements; #endregion #region Constructors public RootNodes(Data data, NodeMetadata nodeMetadata) { foreach (IBaseNodeMixin node in data.document.Flatten()) { switch (node) { case CanvasNode canvasNode: canvases.Add(canvasNode); break; case ComponentSetNode componentSetNode: componentSets.Add(componentSetNode); break; case FrameNode frameNode when node.parent is CanvasNode: frames.Add(frameNode); break; case DefaultShapeNode defaultShapeNode when nodeMetadata.GetTemplate(defaultShapeNode) is (var isHash, { } template) && template.NotNullOrEmpty(): if (!isHash && elements.Any(x => x.hash == template)) { Debug.LogWarning($"Duplicate hash was found: {template}. This might happen when [{nameof(QueryAttribute)}] is inherited in multiple classes. " + "This could also happen when you have a template with the same name. In order to fix that in that case, please use \"Hash = true\" parameter."); break; } elements.Add((defaultShapeNode, template)); break; } } } #endregion } } ================================================ FILE: Editor/Core/RootNodes.cs.meta ================================================ fileFormatVersion: 2 guid: 54369dcc94564417aee21cea7980006a timeCreated: 1733321003 ================================================ FILE: Editor/Core/StylesPreprocessor.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace Figma.Core { using Assets; using Uss; using Internals; using static Const; internal class StylesPreprocessor { static readonly Regex multipleDashesRegex = new("-{2,}", RegexOptions.Compiled); static readonly Regex invalidCharsRegex = new("[^a-zA-Z0-9]", RegexOptions.Compiled); #region Fields readonly List<(StyleSlot slot, UssStyle style)> styles = new(initialCollectionCapacity); readonly List components = new(initialCollectionCapacity); readonly List> componentsStyles = new(initialCollectionCapacity); readonly Dictionary componentStyleMap = new(initialCollectionCapacity); readonly Dictionary nodeStyleMap = new(initialCollectionCapacity); readonly AssetsInfo assetsInfo; readonly Data data; #endregion #region Properties internal IReadOnlyList<(StyleSlot slot, UssStyle style)> Styles => styles; internal IReadOnlyDictionary NodeStyleMap => nodeStyleMap; #endregion internal StylesPreprocessor(Data data, AssetsInfo assetsInfo) { this.data = data; this.assetsInfo = assetsInfo; AddStyles(data.document, data.styles); AddRichText(data.document); for (int i = 0; i < components.Count; i++) { AddStyles(components[i], componentsStyles[i]); AddRichText(components[i]); } InheritStyles(data.document); AddTransitionStyles(); } #region Methods void AddStyles(IBaseNodeMixin root, Dictionary styles) { string GetClassName(string name, string prefix = "n") { const char separator = '-'; if (name.Length > 64) name = name[..64]; name = invalidCharsRegex.Replace(name, separator.ToString()); name = multipleDashesRegex.Replace(name, separator.ToString()); name = name.Trim(separator); if (string.IsNullOrEmpty(name) || name.All(c => c == separator)) name = prefix; if (char.IsDigit(name[0])) name = $"{prefix}-{name}"; return name; } HashSet insideComponents = new(); foreach (IBaseNodeMixin node in root.Flatten()) { bool insideComponent = node is ComponentNode || insideComponents.Contains(node.parent); if (!insideComponent) { UssStyle style = new(GetClassName(node.name), assetsInfo, (BaseNode)node); if (node is ComponentSetNode) { // Removing annoying borders for ComponentSetNode style.Attributes.Clear(); style.Attributes.Add("overflow", "hidden"); } nodeStyleMap[node] = style; } else { insideComponents.Add(node); componentStyleMap[node] = new UssStyle(GetClassName(node.name), assetsInfo, (BaseNode)node); } if (node is not IBlendMixin { styles: not null } blend) continue; foreach ((string styleType, string styleId) in blend.styles) { bool text = node.type == NodeType.TEXT; string slot = styleType; if (slot[^1] == 's') // Sometimes named 'fill' and sometimes 'fills'. We treat them equally. slot = slot[..^1]; if (!this.styles.Any(x => x.slot.Text == text && x.slot.Slot == slot && x.slot.key == styles[styleId].key)) { StyleSlot styleDescriptor = new StyleSlot(text, slot, styles[styleId]); string className = GetClassName(styleDescriptor.name, "s"); UssStyle ussStyle = new UssStyle(className, assetsInfo, (BaseNode)node, styleDescriptor); this.styles.Add((styleDescriptor, ussStyle)); } } } } void AddRichText(IBaseNodeMixin node) { foreach (TextNode textNode in node.Flatten().OfType().Where(x => x.lineTypes is { Length: > 1 } && x.lineTypes.Any(lineType => lineType is LineType.ORDERED or LineType.UNORDERED) || (x.styleOverrideTable != null && x.styleOverrideTable.Any()))) textNode.characters = new RichText.TextBuilder(textNode).Build(); } void AddTransitionStyles() { ComponentNode GetTransitionNode(ComponentSetNode componentSet, ComponentNode defaultComponent, TriggerType triggerType) { Action action = defaultComponent.interactions .Where(interaction => interaction.trigger.type == triggerType) .Select(interaction => interaction.actions.FirstOrDefault()) .FirstOrDefault(action => action?.destinationId != null); string destinationId = action?.destinationId; ComponentNode node = (ComponentNode)componentSet.children.FirstOrDefault(component => component is ComponentNode && component.id == destinationId); return node; } UssStyle GetStyle(Dictionary componentStyleMap, ComponentSetNode componentSet, ComponentNode defaultComponent, TriggerType triggerType) { UssStyle style = null; ComponentNode node = GetTransitionNode(componentSet, defaultComponent, triggerType); if (node != null) componentStyleMap.TryGetValue(node, out style); return style; } UssStyle visualElement = new(nameof(UnityEngine.UIElements.VisualElement)); foreach ((IBaseNodeMixin key, UssStyle componentSetStyle) in nodeStyleMap) { if (key is not ComponentSetNode componentSet) continue; ComponentNode defaultComponent = null; Action action = null; foreach (SceneNode sceneNode in componentSet.children) { if (sceneNode is not ComponentNode componentNode) continue; Interactions activeInteraction = componentNode.interactions.FirstOrDefault(interaction => interaction.trigger.type == TriggerType.ON_HOVER || interaction.trigger.type == TriggerType.ON_CLICK); if (activeInteraction == null) continue; action = activeInteraction.actions.FirstOrDefault(x => x.destinationId != null); if (action == null) continue; defaultComponent = componentNode; UssStyle subStyle = new(componentSetStyle.Name) { Target = visualElement }; if (action.transition != null) { subStyle.transitionDuration = action.transition.duration * 1000; subStyle.transitionEasing = (EasingFunction)action.transition.easing.type; } componentSetStyle.SubStyles.Add(subStyle); break; } if (defaultComponent == null) continue; componentStyleMap.TryGetValue(defaultComponent, out UssStyle idleStyle); UssStyle hoverStyle = GetStyle(componentStyleMap, componentSet, defaultComponent, TriggerType.ON_HOVER); UssStyle clickStyle = GetStyle(componentStyleMap, componentSet, defaultComponent, TriggerType.ON_CLICK); if (idleStyle == null) continue; void InjectSubStyles(ComponentNode node, IReadOnlyList defaultStyles, PseudoClass pseudoClass) { IReadOnlyList styles = GetStyles(node); for (int i = 0; i < styles.Count; i++) { UssStyle style = styles[i]; UssStyle defaultStyle = defaultStyles[i]; componentSetStyle.SubStyles.Add(new UssStyle(componentSetStyle.Name) { PseudoClass = pseudoClass, Target = defaultStyle }.CopyFrom(style)); } } if (action.transition is { type: TransitionType.SMART_ANIMATE }) { ComponentNode hoverNode = GetTransitionNode(componentSet, defaultComponent, TriggerType.ON_HOVER); ComponentNode clickNode = GetTransitionNode(componentSet, defaultComponent, TriggerType.ON_CLICK); IReadOnlyList defaultStyles = GetStyles(defaultComponent); if (hoverNode != null) InjectSubStyles(hoverNode, defaultStyles, PseudoClass.Hover); if (clickNode != null) InjectSubStyles(clickNode, defaultStyles, PseudoClass.Active); } if (action.transition is { type: TransitionType.DISSOLVE }) componentSetStyle.SubStyles.AddRange(UssStyle.MakeTransitionStyles(componentSetStyle, idleStyle, hoverStyle, clickStyle)); } } internal void AddMissingComponent(ComponentNode component, Dictionary componentStyles) { components.Add(component); componentsStyles.Add(componentStyles); } #endregion #region Support Methods internal IReadOnlyList GetStyles(IBaseNodeMixin root) => root.Flatten(node => node.IsVisible() && node is not ComponentSetNode) .Select(node => componentStyleMap.TryGetValue(node, out UssStyle style) || nodeStyleMap.TryGetValue(node, out style) ? style : null) .Where(style => style is not null) .ToList(); void InheritStyles(IBaseNodeMixin root) { List styles = new(); foreach (IBaseNodeMixin node in root.Flatten(x => x.parent is not BooleanOperationNode)) { UssStyle style = GetStyle(node); if (node is IBlendMixin { styles: not null } blend) { foreach (KeyValuePair keyValue in blend.styles) { bool text = node.type == NodeType.TEXT; string styleType = keyValue.Key; if (styleType[^1] == 's') styleType = styleType[..^1]; string styleId = keyValue.Value; string key = null; if (data.styles.TryGetValue(styleId, out Style documentStyle)) key = documentStyle.key; foreach (Dictionary componentStyle in componentsStyles) if (componentStyle.TryGetValue(styleId, out Style value)) key = value.key; int index; if (key.NotNullOrEmpty() && (index = this.styles.FindIndex(x => x.slot.Text == text && x.slot.Slot == styleType && x.slot.key == key)) >= 0) styles.Add(this.styles[index].style); } } if (styles.Count > 0) style.Inherit(styles); styles.Clear(); } } internal string GetClassList(IBaseNodeMixin node) { UssStyle style = GetStyle(node); if (style == null) return string.Empty; List styles = new List(); if (node is IBlendMixin { styles: not null } blend) { foreach (KeyValuePair keyValue in blend.styles) { bool text = node.type == NodeType.TEXT; string styleType = keyValue.Key; if (styleType[^1] == 's') styleType = styleType[..^1]; string styleId = keyValue.Value; string key = null; if (data.styles.TryGetValue(styleId, out Style documentStyle)) key = documentStyle.key; foreach (Dictionary componentStyle in componentsStyles) if (componentStyle.TryGetValue(styleId, out Style value)) key = value.key; int index; if (key.NotNullOrEmpty() && (index = this.styles.FindIndex(x => x.slot.Text == text && x.slot.Slot == styleType && x.slot.key == key)) >= 0) styles.Add(this.styles[index].style.Name); } } if (node.IsSvgNode()) styles.Clear(); List classes = new List(); classes.Add(UssStyle.overrideClass.Name); if (style.Attributes.Count > 0) classes.Add(style.Name); classes.AddRange(styles); if (node.IsRootNode()) classes.Add(UssStyle.viewportClass.Name); return string.Join(" ", classes); } UssStyle GetStyle(IBaseNodeMixin node) => componentStyleMap.TryGetValue(node, out UssStyle style) || nodeStyleMap.TryGetValue(node, out style) ? style : null; #endregion } } ================================================ FILE: Editor/Core/StylesPreprocessor.cs.meta ================================================ fileFormatVersion: 2 guid: 756f15f2e8e54ce6b41e0fdaa5aa7fe8 timeCreated: 1742559104 ================================================ FILE: Editor/Core/Uss/BaseUssStyle.cs ================================================ using Figma.Internals; using System; using System.Collections.Generic; using System.Linq; namespace Figma.Core.Uss { internal abstract class BaseUssStyle { #region Fields readonly List inherited = new(); #endregion #region Properties public string Name { get; set; } public PseudoClass PseudoClass { get; set; } public BaseUssStyle Target { get; set; } public List SubStyles { get; } = new(); public Dictionary Attributes { get; } = new(); public bool HasAttributes => Attributes.Count > 0; #endregion #region Constructors protected BaseUssStyle(string name) => Name = name; #endregion #region Methods public string BuildName() { string result = $".{Name}"; if (PseudoClass is not PseudoClass.None) result += $":{PseudoClass.ToString().ToLower()}"; if (Target == null) return result; result += " > "; if (Target.Name is not (nameof(UnityEngine.UIElements.VisualElement) or nameof(UnityEngine.UIElements.Button))) result += "."; result += Target.Name; return result; } public bool DoesInherit(BaseUssStyle style) => inherited.Contains(style); public void Inherit(IReadOnlyCollection styles) { inherited.AddRange(styles); styles.SelectMany(style => style.Attributes.Where(keyValue => Attributes.TryGetValue(keyValue.Key, out string value) && value == keyValue.Value)) .Select(x => x.Key) .ForEach(key => Attributes.Remove(key)); } #endregion #region Support Methods protected string Get(string name) => Attributes[name]; protected string GetDefault(string name, string defaultValue) => Attributes.ContainsKey(name) ? Attributes[name] : defaultValue; protected string Get1(string name, string group, int index) { if (Attributes.TryGetValue(group, out string groupValue)) { Length4Property length4 = groupValue; return length4[index]; } if (Attributes.TryGetValue(name, out string nameValue)) return nameValue; throw new NotSupportedException(); } protected string Get4(string name, params string[] names) { if (Attributes.TryGetValue(name, out string value)) return value; LengthProperty[] properties = new LengthProperty[4]; for (int i = 0; i < 4; ++i) properties[i] = Attributes.TryGetValue(names[i], out string indexedValue) ? indexedValue : new LengthProperty(Unit.Pixel); return new Length4Property(properties); } protected void Set(string name, string value) => Attributes[name] = value; protected void Set1(string name, string value, params string[] names) { Attributes[name] = value; for (int i = 0; i < 4; ++i) Attributes.Remove(names[i]); } protected void Set4(string name, string value, string group, int index) { if (Attributes.TryGetValue(group, out string item)) { Length4Property length4 = item; length4[index] = value; Set(group, length4); } else Set(name, value); } protected static string Url(string url) => $"url('{url}')"; protected static string Resource(string resource) => $"resource('{resource}')"; #endregion } } ================================================ FILE: Editor/Core/Uss/BaseUssStyle.cs.meta ================================================ fileFormatVersion: 2 guid: 6a8ef63978cd415290c6cbe844cd7f9b timeCreated: 1732193154 ================================================ FILE: Editor/Core/Uss/Properties/AssetProperty.cs ================================================ using System; namespace Figma.Core.Uss { using Internals; /// /// Represents an asset in a Resources folder or represents an asset specified by a path, it can be expressed as either a relative path or an absolute path. /// internal struct AssetProperty { #region Fields readonly string url; readonly string resource; readonly Unit unit; #endregion #region Constructors AssetProperty(Unit unit) { url = null; resource = null; this.unit = unit; } AssetProperty(string value) { url = null; resource = null; unit = default; if (value.StartsWith(nameof(url))) url = value; else if (value.StartsWith(nameof(resource))) resource = value; else throw new NotSupportedException(); } #endregion #region Operators public static implicit operator AssetProperty(Unit value) => new(value); public static implicit operator AssetProperty(string value) => Enum.TryParse(value, true, out Unit unit) ? new AssetProperty(unit) : new AssetProperty(value); public static implicit operator string(AssetProperty value) { if (value.url.NotNullOrEmpty()) return value.url; if (value.resource.NotNullOrEmpty()) return value.resource; return value.unit switch { Unit.None => "none", Unit.Initial => "initial", _ => throw new ArgumentException(nameof(value)) }; } #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/AssetProperty.cs.meta ================================================ fileFormatVersion: 2 guid: b1d01eee68b444c88e74da4382b53373 timeCreated: 1727946243 ================================================ FILE: Editor/Core/Uss/Properties/ColorProperty.cs ================================================ using System; namespace Figma.Core.Uss { using Internals; using Const = Const; /// /// Represents a color. You can define a color with a #hexadecimal code, the rgb() or rgba() function, or a color keyword (for example, blue or transparent). /// internal readonly struct ColorProperty { #region Fields readonly string rgba; readonly string rgb; readonly string hex; readonly string name; #endregion #region Constructors internal ColorProperty(RGBA color, Double? opacity = 1, float alphaMult = 1) { rgba = $"rgba({(byte)(color.r * 255.0f)},{(byte)(color.g * 255.0f)},{(byte)(color.b * 255.0f)},{(color.a * (opacity ?? alphaMult)).ToString("F2", Const.Culture).Replace(".00", string.Empty)})"; rgb = null; hex = null; name = null; } ColorProperty(string value) { rgba = null; rgb = null; hex = null; name = null; if (value.StartsWith(nameof(rgba))) rgba = value; else if (value.StartsWith(nameof(rgb))) rgb = value; else if (value.StartsWith('#')) hex = value; else name = value; } #endregion #region Operators public static implicit operator ColorProperty(Unit _) => new(); public static implicit operator ColorProperty(RGBA value) => new(value); public static implicit operator ColorProperty(string value) => new(value); public static implicit operator string(ColorProperty value) { if (value.rgba.NotNullOrEmpty()) return value.rgba; if (value.rgb.NotNullOrEmpty()) return value.rgb; if (value.hex.NotNullOrEmpty()) return value.hex; if (value.name.NotNullOrEmpty()) return value.name; return "initial"; } public override string ToString() => this; #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/ColorProperty.cs.meta ================================================ fileFormatVersion: 2 guid: 11225bd9ebe040f49fab329a81bc3f88 timeCreated: 1727946232 ================================================ FILE: Editor/Core/Uss/Properties/CursorProperty.cs ================================================ namespace Figma.Core.Uss { internal struct CursorProperty { #region Operators public static implicit operator CursorProperty(string _) => new(); public static implicit operator string(CursorProperty _) => null; #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/CursorProperty.cs.meta ================================================ fileFormatVersion: 2 guid: 688b2f12fd56420486ab60eaa3f520c7 timeCreated: 1727946311 ================================================ FILE: Editor/Core/Uss/Properties/DurationProperty.cs ================================================ using System; namespace Figma.Core.Uss { /// /// Represents a duration value from Figma API. /// internal readonly struct DurationProperty { #region Fields readonly double value; readonly TimeUnit unit; #endregion #region Constructors internal DurationProperty(TimeUnit unit) { value = 0; this.unit = unit; } internal DurationProperty(double value, TimeUnit unit) { this.value = value; this.unit = unit; } #endregion #region Operators public static implicit operator DurationProperty(TimeUnit value) => new(0, value); public static implicit operator DurationProperty(double? value) => new(value!.Value, TimeUnit.Millisecond); public static implicit operator DurationProperty(double value) => new(value, TimeUnit.Millisecond); public static implicit operator DurationProperty(string value) { if (Enum.TryParse(value, true, out TimeUnit unit)) return new DurationProperty(unit); if (value.ToLower(Const.Culture).Contains("ms")) return new DurationProperty(double.Parse(value.ToLower(Const.Culture).Replace("ms", string.Empty), Const.Culture), TimeUnit.Millisecond); if (value.ToLower(Const.Culture).Contains("s")) return new DurationProperty(double.Parse(value.ToLower(Const.Culture).Replace("s", string.Empty), Const.Culture), TimeUnit.Second); return default; } public static implicit operator string(DurationProperty value) { return value.unit switch { TimeUnit.Default => $"0ms", TimeUnit.Millisecond => $"{value.value.ToString("F2", Const.Culture).Replace(".00", string.Empty)}ms", TimeUnit.Second => $"{value.value.ToString("F2", Const.Culture).Replace(".00", string.Empty)}s", _ => throw new ArgumentException(nameof(value)) }; } public static DurationProperty operator +(DurationProperty a) => a; public static DurationProperty operator -(DurationProperty a) => new(-a.value, a.unit); public static DurationProperty operator +(DurationProperty a, double b) => new(a.value + b, a.unit); public static DurationProperty operator -(DurationProperty a, double b) => new(a.value - b, a.unit); public static bool operator ==(DurationProperty a, TimeUnit b) => a.unit == b; public static bool operator !=(DurationProperty a, TimeUnit b) => a.unit != b; public override bool Equals(object obj) => obj is DurationProperty property && value == property.value && unit == property.unit; public override int GetHashCode() => HashCode.Combine(value, unit); public override string ToString() => this; #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/DurationProperty.cs.meta ================================================ fileFormatVersion: 2 guid: f01b58e2e2c44e75b07f54061e244f90 timeCreated: 1732271697 ================================================ FILE: Editor/Core/Uss/Properties/EnumProperty.cs ================================================ using System; using System.Text.RegularExpressions; namespace Figma.Core.Uss { #pragma warning disable CS0660, CS0661 internal struct EnumProperty where T : struct, Enum #pragma warning restore CS0660, CS0661 { // ReSharper disable StaticMemberInGenericType static readonly Regex enumParserRegexString = new("(?([a-z]+\\-?))", RegexOptions.Compiled); static readonly Regex enumParserRegexValue = new("(?([A-Z][a-z]+)?)", RegexOptions.Compiled); // ReSharper restore StaticMemberInGenericType #region Fields T value; readonly Unit unit; #endregion #region Constructors EnumProperty(T value) { this.value = value; unit = Unit.None; } EnumProperty(Unit unit) { value = default; this.unit = unit; } #endregion #region Operators public static implicit operator EnumProperty(Unit unit) => new(unit); public static implicit operator EnumProperty(T value) => new(value); public static implicit operator EnumProperty(string value) => Enum.TryParse(enumParserRegexString.Replace(value, "${name}").Replace("-", string.Empty), true, out T result) ? new EnumProperty(result) : default; public static implicit operator string(EnumProperty value) => value.unit == Unit.None ? enumParserRegexValue.Replace(value.value.ToString(), "${name}-").ToLower().TrimEnd('-') : "initial"; public static bool operator ==(EnumProperty a, T b) => a.value.Equals(b); public static bool operator !=(EnumProperty a, T b) => !a.value.Equals(b); public override string ToString() => this; #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/EnumProperty.cs.meta ================================================ fileFormatVersion: 2 guid: 911a38bb38a64ff98a53324b53da101b timeCreated: 1727946257 ================================================ FILE: Editor/Core/Uss/Properties/FlexProperty.cs ================================================ namespace Figma.Core.Uss { internal struct FlexProperty { #region Operators public static implicit operator FlexProperty(string _) => new(); public static implicit operator string(FlexProperty _) => null; #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/FlexProperty.cs.meta ================================================ fileFormatVersion: 2 guid: c6e36b8acd8548b0add9ddf1a37509b0 timeCreated: 1727946301 ================================================ FILE: Editor/Core/Uss/Properties/IntegerProperty.cs ================================================ namespace Figma.Core.Uss { /// /// Represents a whole number. /// internal struct IntegerProperty { #region Fields readonly int value; #endregion #region Constructors IntegerProperty(int value) => this.value = value; #endregion #region Operators public static implicit operator IntegerProperty(int? value) => new(value!.Value); public static implicit operator IntegerProperty(int value) => new(value); public static implicit operator IntegerProperty(string value) => new(int.Parse(value)); public static implicit operator string(IntegerProperty value) => value.value.ToString(Const.Culture); public static IntegerProperty operator +(IntegerProperty a) => a; public static IntegerProperty operator -(IntegerProperty a) => new(-a.value); public static IntegerProperty operator +(IntegerProperty a, int b) => new(a.value + b); public static IntegerProperty operator -(IntegerProperty a, int b) => new(a.value - b); #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/IntegerProperty.cs.meta ================================================ fileFormatVersion: 2 guid: a466380f063f4ec98d082d2ec7cd1f20 timeCreated: 1727946212 ================================================ FILE: Editor/Core/Uss/Properties/LayoutDouble4.cs ================================================ using System; namespace Figma.Core.Uss { struct LayoutDouble4 { #region Fields public double top; public double right; public double bottom; public double left; #endregion #region Methods public LayoutDouble4(double top, double right, double bottom, double left) { this.top = top; this.right = right; this.bottom = bottom; this.left = left; } public LayoutDouble4(double value) { top = value; right = value; bottom = value; left = value; } public LayoutDouble4 OnlyPositiveValues() => new(top > UssStyle.tolerance ? top : 0.0, right > UssStyle.tolerance ? right : 0.0, bottom > UssStyle.tolerance ? bottom : 0.0, left > UssStyle.tolerance ? left : 0.0); public LayoutDouble4 OnlyNegativeValues() => new(top < UssStyle.tolerance ? top : 0.0, right < UssStyle.tolerance ? right : 0.0, bottom < UssStyle.tolerance ? bottom : 0.0, left < UssStyle.tolerance ? left : 0.0); public Length4Property ToLength4Property() => new[] { top, right, bottom, left }; public bool Any() => Math.Abs(top) > UssStyle.tolerance || Math.Abs(right) > UssStyle.tolerance || Math.Abs(bottom) > UssStyle.tolerance || Math.Abs(left) > UssStyle.tolerance; public static LayoutDouble4 operator +(LayoutDouble4 a, LayoutDouble4 b) => new(a.top + b.top, a.right + b.right, a.bottom + b.bottom, a.left + b.left); public static LayoutDouble4 operator -(LayoutDouble4 a, LayoutDouble4 b) => new(a.top - b.top, a.right - b.right, a.bottom - b.bottom, a.left - b.left); public static LayoutDouble4 operator -(LayoutDouble4 a) => new(-a.top, -a.right, -a.bottom, -a.left); public static LayoutDouble4 operator *(LayoutDouble4 a, double k) => new(a.top * k, a.right * k, a.bottom * k, a.left * k); #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/LayoutDouble4.cs.meta ================================================ fileFormatVersion: 2 guid: 3c58a19efd2f4860a12c1aca6d120164 timeCreated: 1743523469 ================================================ FILE: Editor/Core/Uss/Properties/Length2Property.cs ================================================ using System; using System.Linq; namespace Figma.Core.Uss { internal readonly struct Length2Property { #region Fields readonly Unit unit; readonly LengthProperty[] properties; #endregion #region Properties internal LengthProperty this[int index] { get => properties[index]; set => properties[index] = value; } #endregion #region Constructors internal Length2Property(Unit unit) { this.unit = unit; properties = new LengthProperty[] { new(unit), new(unit) }; } internal Length2Property(LengthProperty[] properties) { unit = Unit.None; this.properties = properties; } #endregion #region Operators public static implicit operator Length2Property(Unit unit) => new(unit); public static implicit operator Length2Property(Double? value) => new(new LengthProperty[] { value!.Value }); public static implicit operator Length2Property(Double value) => new(new LengthProperty[] { value }); public static implicit operator Length2Property(Double[] values) { LengthProperty[] properties = new LengthProperty[values.Length]; for (int i = 0; i < values.Length; i++) properties[i] = values[i]; return new Length2Property(properties); } public static implicit operator Length2Property(string value) { string[] values = value.Split(" ", StringSplitOptions.RemoveEmptyEntries); LengthProperty[] properties = new LengthProperty[values.Length]; for (int i = 0; i < values.Length; i++) properties[i] = values[i]; return new Length2Property(properties); } public static implicit operator string(Length2Property value) { if (value is { unit: Unit.None, properties: not null }) { string[] values = new string[value.properties.Length]; for (int i = 0; i < values.Length; i++) values[i] = value.properties[i]; return string.Join(" ", values); } return new LengthProperty(value.unit); } public static Length2Property operator +(Length2Property a) => a; public static Length2Property operator -(Length2Property a) => new(a.properties.Select(x => -x).ToArray()); public static Length2Property operator +(Length2Property a, Double b) => new(a.properties.Select(x => x + b).ToArray()); public static Length2Property operator -(Length2Property a, Double b) => new(a.properties.Select(x => x - b).ToArray()); #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/Length2Property.cs.meta ================================================ fileFormatVersion: 2 guid: ac3787c7e7634fc9983559df65387add timeCreated: 1738319788 ================================================ FILE: Editor/Core/Uss/Properties/Length4Property.cs ================================================ using System; using System.Linq; namespace Figma.Core.Uss { internal readonly struct Length4Property { #region Fields readonly Unit unit; readonly LengthProperty[] properties; #endregion #region Properties internal LengthProperty this[int index] { get => properties[index]; set => properties[index] = value; } #endregion #region Constructors internal Length4Property(Unit unit) { this.unit = unit; properties = new LengthProperty[] { new(unit), new(unit), new(unit), new(unit) }; } internal Length4Property(LengthProperty[] properties) { unit = Unit.None; this.properties = properties; } #endregion #region Operators public static implicit operator Length4Property(Unit unit) => new(unit); public static implicit operator Length4Property(double? value) => new(new LengthProperty[] { value!.Value }); public static implicit operator Length4Property(double value) => new(new LengthProperty[] { value }); public static implicit operator Length4Property(double[] values) => new(values.Select(x => (LengthProperty)x).ToArray()); public static implicit operator Length4Property(string value) => new(value.Split(" ", StringSplitOptions.RemoveEmptyEntries).Select(x => (LengthProperty)x).ToArray()); public static implicit operator string(Length4Property value) => value is { unit: Unit.None, properties: not null } ? string.Join(" ", value.properties.Select(p => (string)p)) : new LengthProperty(value.unit); public static Length4Property operator +(Length4Property a) => a; public static Length4Property operator -(Length4Property a) => new(a.properties.Select(x => -x).ToArray()); public static Length4Property operator +(Length4Property a, double b) => new(a.properties.Select(x => x + b).ToArray()); public static Length4Property operator -(Length4Property a, double b) => new(a.properties.Select(x => x - b).ToArray()); #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/Length4Property.cs.meta ================================================ fileFormatVersion: 2 guid: 22ebc27065094f06894a5301834db528 timeCreated: 1727946288 ================================================ FILE: Editor/Core/Uss/Properties/LengthProperty.cs ================================================ using System; namespace Figma.Core.Uss { /// /// Represents a distance value. /// internal readonly struct LengthProperty { #region Fields readonly double value; readonly Unit unit; #endregion #region Constructors internal LengthProperty(Unit unit) { value = 0; this.unit = unit; } internal LengthProperty(double value, Unit unit) { this.value = value; this.unit = unit; } #endregion #region Operators public static implicit operator LengthProperty(Unit value) => new(0, value); public static implicit operator LengthProperty(double? value) => new(value!.Value, Unit.Pixel); public static implicit operator LengthProperty(double value) => new(value, Unit.Pixel); public static implicit operator LengthProperty(string value) { if (Enum.TryParse(value, true, out Unit unit)) return new LengthProperty(unit); if (value.Contains("px")) return new LengthProperty(double.Parse(value.ToLower().Replace("px", string.Empty), Const.Culture), Unit.Pixel); if (value.Contains("deg")) return new LengthProperty(double.Parse(value.ToLower().Replace("deg", string.Empty), Const.Culture), Unit.Degrees); if (value.Contains('%')) return new LengthProperty(double.Parse(value.Replace("%", string.Empty), Const.Culture), Unit.Percent); return default; } public static implicit operator string(LengthProperty value) { return value.unit switch { Unit.Pixel => value.value == 0.0 ? "0" : $"{(int)Math.Round(value.value)}px", Unit.Degrees => value.value == 0.0 ? "0" : $"{value.value.ToString("F2", Const.Culture).Replace(".00", string.Empty)}deg", Unit.Percent => value.value == 0.0 ? "0" : $"{value.value.ToString("F2", Const.Culture).Replace(".00", string.Empty)}%", Unit.Auto => "auto", Unit.None => "none", Unit.Initial => "initial", Unit.Default => "0", _ => throw new ArgumentException(nameof(value)) }; } public static LengthProperty operator +(LengthProperty a) => a; public static LengthProperty operator -(LengthProperty a) => new(-a.value, a.unit); public static LengthProperty operator +(LengthProperty a, double b) => new(a.value + b, a.unit); public static LengthProperty operator -(LengthProperty a, double b) => new(a.value - b, a.unit); public static bool operator ==(LengthProperty a, Unit b) => a.unit == b; public static bool operator !=(LengthProperty a, Unit b) => a.unit != b; public override bool Equals(object obj) => obj is LengthProperty property && value == property.value && unit == property.unit; public override int GetHashCode() => HashCode.Combine(value, unit); public override string ToString() => this; #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/LengthProperty.cs.meta ================================================ fileFormatVersion: 2 guid: 50407f2db68e4ab3ac1c3fe00a5b98f7 timeCreated: 1727946151 ================================================ FILE: Editor/Core/Uss/Properties/NumberProperty.cs ================================================ namespace Figma.Core.Uss { /// /// Represents either an integer or a number with a fractional component. /// internal struct NumberProperty { #region Fields readonly double value; #endregion #region Constructors NumberProperty(double value) => this.value = value; #endregion #region Operators public static implicit operator NumberProperty(double? value) => new(value!.Value); public static implicit operator NumberProperty(double value) => new(value); public static implicit operator NumberProperty(string value) => new(double.Parse(value, Const.Culture)); public static implicit operator string(NumberProperty value) => value.value.ToString("F2", Const.Culture).Replace(".00", string.Empty); public static NumberProperty operator +(NumberProperty a) => a; public static NumberProperty operator -(NumberProperty a) => new(-a.value); public static NumberProperty operator +(NumberProperty a, double b) => new(a.value + b); public static NumberProperty operator -(NumberProperty a, double b) => new(a.value - b); #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/NumberProperty.cs.meta ================================================ fileFormatVersion: 2 guid: 674b330b5aea49318570f61fabe5e9cb timeCreated: 1727946179 ================================================ FILE: Editor/Core/Uss/Properties/ShadowProperty.cs ================================================ using System.Text.RegularExpressions; namespace Figma.Core.Uss { internal struct ShadowProperty { static readonly Regex regex = new(@"(?\d+[px]*)\s+(?\d+[px]*)\s+(?\d+[px]*)\s+(?(rgba\([\d,\.\s]+\))|#\w{2,8}|[^#][\w-]+)"); #region Fields readonly LengthProperty offsetHorizontal; readonly LengthProperty offsetVertical; readonly LengthProperty blurRadius; readonly ColorProperty color; #endregion #region Constructors internal ShadowProperty(LengthProperty offsetHorizontal, LengthProperty offsetVertical, LengthProperty blurRadius, ColorProperty color) { this.offsetHorizontal = offsetHorizontal; this.offsetVertical = offsetVertical; this.blurRadius = blurRadius; this.color = color; } ShadowProperty(string value) { Match match = regex.Match(value); offsetHorizontal = match.Groups[nameof(offsetHorizontal)].Value; offsetVertical = match.Groups[nameof(offsetVertical)].Value; blurRadius = match.Groups[nameof(blurRadius)].Value; color = match.Groups[nameof(color)].Value; } #endregion #region Operators public static implicit operator ShadowProperty(string value) => new(value); public static implicit operator string(ShadowProperty value) => $"{value.offsetHorizontal} {value.offsetVertical} {value.blurRadius} {value.color}"; #endregion } } ================================================ FILE: Editor/Core/Uss/Properties/ShadowProperty.cs.meta ================================================ fileFormatVersion: 2 guid: c3584d21c614453fa132bc171f4420eb timeCreated: 1727946275 ================================================ FILE: Editor/Core/Uss/Properties.meta ================================================ fileFormatVersion: 2 guid: 049f021dc7f44bb389f9f132ed103b7c timeCreated: 1727946137 ================================================ FILE: Editor/Core/Uss/StyleSlot.cs ================================================ namespace Figma.Core.Uss { using Internals; internal class StyleSlot : Style { #region Fields public bool Text { get; } public string Slot { get; } #endregion #region Constructors public StyleSlot(bool text, string slot, Style style) { Text = text; Slot = slot; styleType = style.styleType; key = style.key; name = style.name; description = style.description; } #endregion #region Methods public override string ToString() => $"text={Text} slot={Slot} styleType={styleType} key={key} name={name} description={description}"; #endregion } } ================================================ FILE: Editor/Core/Uss/StyleSlot.cs.meta ================================================ fileFormatVersion: 2 guid: 7cb6785b010e457392b9a283f2cd0e62 timeCreated: 1727945310 ================================================ FILE: Editor/Core/Uss/UssStyle.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Figma.Core.Uss { using Assets; using Internals; internal class UssStyle : BaseUssStyle { /// Problem with figma 2 Unity is that, Unity’s box-sizing property is always border-box, while figma's is a content-box with changing borders. See the MDN documentation https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing #region Const public const double tolerance = 0.01; internal static readonly UssStyle overrideClass = new("unity-base-override") { alignItems = Align.Center, backgroundColor = Unit.Initial, borderWidth = Unit.Initial, justifyContent = JustifyContent.Center, margin = Unit.Initial, overflow = Unit.Initial, padding = Unit.Initial, unityBackgroundPositionX = BackgroundPositionKeyword.Center, unityBackgroundPositionY = BackgroundPositionKeyword.Center, unityBackgroundRepeat = Repeat.NoRepeat, unityFontDefinition = Unit.Initial, }; internal static readonly UssStyle viewportClass = new("unity-viewport") { position = Position.Absolute, width = "100%", height = "100%" }; #endregion #region Fields readonly AssetsInfo assetsInfo; #endregion #region Properties // Box model // Dimensions LengthProperty width { get => Get(nameof(width)); set => Set(nameof(width), value); } LengthProperty height { get => Get(nameof(height)); set => Set(nameof(height), value); } LengthProperty minWidth { get => Get("min-width"); set => Set("min-width", value); } LengthProperty minHeight { get => Get("min-height"); set => Set("min-height", value); } LengthProperty maxWidth { get => Get("max-width"); set => Set("max-width", value); } LengthProperty maxHeight { get => Get("max-height"); set => Set("max-height", value); } // Margins LengthProperty marginLeft { get => Get1("margin-left", nameof(margin), 0); set => Set4("margin-left", value, nameof(margin), 0); } LengthProperty marginTop { get => Get1("margin-top", nameof(margin), 1); set => Set4("margin-top", value, nameof(margin), 1); } LengthProperty marginRight { get => Get1("margin-right", nameof(margin), 2); set => Set4("margin-right", value, nameof(margin), 2); } LengthProperty marginBottom { get => Get1("margin-bottom", nameof(margin), 3); set => Set4("margin-bottom", value, nameof(margin), 3); } Length4Property margin { get => Get4(nameof(margin), "margin-left", "margin-top", "margin-right", "margin-bottom"); set => Set1(nameof(margin), value, "margin-left", "margin-top", "margin-right", "margin-bottom"); } // Borders LengthProperty borderLeftWidth { get => Get1("border-left-width", "border-width", 0); set => Set4("border-left-width", value, "border-width", 0); } LengthProperty borderTopWidth { get => Get1("border-top-width", "border-width", 1); set => Set4("border-top-width", value, "border-width", 1); } LengthProperty borderRightWidth { get => Get1("border-right-width", "border-width", 2); set => Set4("border-right-width", value, "border-width", 2); } LengthProperty borderBottomWidth { get => Get1("border-bottom-width", "border-width", 3); set => Set4("border-bottom-width", value, "border-width", 3); } Length4Property borderWidth { get => Get4("border-width", "border-left-width", "border-top-width", "border-right-width", "border-bottom-width"); set => Set1("border-width", value, "border-left-width", "border-top-width", "border-right-width", "border-bottom-width"); } // Padding LengthProperty paddingLeft { get => Get1("padding-left", nameof(padding), 0); set => Set4("padding-left", value, nameof(padding), 0); } LengthProperty paddingTop { get => Get1("padding-top", nameof(padding), 1); set => Set4("padding-top", value, nameof(padding), 1); } LengthProperty paddingRight { get => Get1("padding-right", nameof(padding), 2); set => Set4("padding-right", value, nameof(padding), 2); } LengthProperty paddingBottom { get => Get1("padding-bottom", nameof(padding), 3); set => Set4("padding-bottom", value, nameof(padding), 3); } Length4Property padding { get => Get4(nameof(padding), "padding-left", "padding-top", "padding-right", "padding-bottom"); set => Set1(nameof(padding), value, "padding-left", "padding-top", "padding-right", "padding-bottom"); } // Flex // Items NumberProperty flexGrow { get => Get("flex-grow"); set => Set("flex-grow", value); } NumberProperty flexShrink { get => Get("flex-shrink"); set => Set("flex-shrink", value); } LengthProperty flexBasis { get => Get("flex-basis"); set => Set("flex-basis", value); } FlexProperty flex { get => Get(nameof(flex)); set => Set(nameof(flex), value); } EnumProperty alignSelf { get => Get("align-self"); set => Set("align-self", value); } NumberProperty itemSpacing { get => Get("--item-spacing"); set => Set("--item-spacing", value); } // Containers EnumProperty flexDirection { get => Get("flex-direction"); set => Set("flex-direction", value); } EnumProperty flexWrap { get => Get("flex-wrap"); set => Set("flex-wrap", value); } EnumProperty alignContent { get => Get("align-content"); set => Set("align-content", value); } EnumProperty alignItems { get => Get("align-items"); set => Set("align-items", value); } EnumProperty justifyContent { get => Get("justify-content"); set => Set("justify-content", value); } // Positioning EnumProperty position { get => Get(nameof(position)); set => Set(nameof(position), value); } LengthProperty left { get => Get(nameof(left)); set => Set(nameof(left), value); } LengthProperty top { get => Get(nameof(top)); set => Set(nameof(top), value); } LengthProperty right { get => Get(nameof(right)); set => Set(nameof(right), value); } LengthProperty bottom { get => Get(nameof(bottom)); set => Set(nameof(bottom), value); } LengthProperty rotate { get => Get(nameof(rotate)); set => Set(nameof(rotate), value); } Length2Property translate { get => GetDefault(nameof(translate), "0 0"); set => Set(nameof(translate), value); } // Drawing // Background ColorProperty backgroundColor { get => Get("background-color"); set => Set("background-color", value); } AssetProperty backgroundImage { get => Get("background-image"); set => Set("background-image", value); } EnumProperty unityBackgroundPositionX { get => Get("background-position-x"); set => Set("background-position-x", value); } EnumProperty unityBackgroundPositionY { get => Get("background-position-y"); set => Set("background-position-y", value); } EnumProperty unityBackgroundRepeat { get => Get("background-repeat"); set => Set("background-repeat", value); } EnumProperty unityBackgroundSize { get => GetDefault("background-size", "auto"); set => Set("background-size", value); } ColorProperty unityBackgroundImageTintColor { get => Get("-unity-background-image-tint-color"); set => Set("-unity-background-image-tint-color", value); } // Slicing IntegerProperty unitySliceLeft { get => Get("-unity-slice-left"); set => Set("-unity-slice-left", value); } IntegerProperty unitySliceTop { get => Get("-unity-slice-top"); set => Set("-unity-slice-top", value); } IntegerProperty unitySliceRight { get => Get("-unity-slice-right"); set => Set("-unity-slice-right", value); } IntegerProperty unitySliceBottom { get => Get("-unity-slice-bottom"); set => Set("-unity-slice-bottom", value); } // Borders ColorProperty borderColor { get => Get("border-color"); set => Set("border-color", value); } LengthProperty borderTopLeftRadius { get => Get1("border-top-left-radius", "border-radius", 0); set => Set4("border-top-left-radius", value, "border-radius", 0); } LengthProperty borderTopRightRadius { get => Get1("border-top-right-radius", "border-radius", 1); set => Set4("border-top-right-radius", value, "border-radius", 1); } LengthProperty borderBottomLeftRadius { get => Get1("border-bottom-left-radius", "border-radius", 2); set => Set4("border-bottom-left-radius", value, "border-radius", 2); } LengthProperty borderBottomRightRadius { get => Get1("border-bottom-right-radius", "border-radius", 3); set => Set4("border-bottom-right-radius", value, "border-radius", 3); } Length4Property borderRadius { get => Get4("border-radius", "border-top-left-radius", "border-top-right-radius", "border-bottom-left-radius", "border-bottom-right-radius"); set => Set1("border-radius", value, "border-top-left-radius", "border-top-right-radius", "border-bottom-left-radius", "border-bottom-right-radius"); } // Appearance EnumProperty overflow { get => Get(nameof(overflow)); set => Set(nameof(overflow), value); } EnumProperty unityOverflowClipBox { get => Get("-unity-overflow-clip-box"); set => Set("-unity-overflow-clip-box", value); } NumberProperty opacity { get => Get(nameof(opacity)); set => Set(nameof(opacity), value); } EnumProperty visibility { get => Get(nameof(visibility)); set => Set(nameof(visibility), value); } EnumProperty display { get => Get(nameof(display)); set => Set(nameof(display), value); } // Text ColorProperty color { get => Get(nameof(color)); set => Set(nameof(color), value); } AssetProperty unityFont { get => Get("-unity-font"); set => Set("-unity-font", value); } AssetProperty unityFontDefinition { get => Get("-unity-font-definition"); set => Set("-unity-font-definition", value); } LengthProperty fontSize { get => Get("font-size"); set => Set("font-size", value); } EnumProperty unityFontStyle { get => Get("-unity-font-style"); set => Set("-unity-font-style", value); } EnumProperty unityTextAlign { get => Get("-unity-text-align"); set => Set("-unity-text-align", value); } EnumProperty whiteSpace { get => Get("white-space"); set => Set("white-space", value); } ShadowProperty textShadow { get => Get("text-shadow"); set => Set("text-shadow", value); } EnumProperty textOverflow { get => Get("text-overflow"); set => Set("text-overflow", value); } // Cursor CursorProperty cursor { get => Get(nameof(cursor)); set => Set(nameof(cursor), value); } // Effects ShadowProperty boxShadow { get => Get("--box-shadow"); set => Set("--box-shadow", value); } // Transitions internal DurationProperty transitionDuration { get => Get("transition-duration"); set => Set("transition-duration", value); } internal EnumProperty transitionEasing { get => Get("transition-timing-function"); set => Set("transition-timing-function", value); } #endregion #region Constructors public UssStyle(string name) : base(name) { } public UssStyle(string name, AssetsInfo assetsInfo) : this(name) => this.assetsInfo = assetsInfo; public UssStyle(string name, AssetsInfo assetsInfo, BaseNode node, StyleSlot styleSlot) : this(name, assetsInfo) { switch (styleSlot.styleType) { case StyleType.FILL when node is IGeometryMixin geometry: switch (styleSlot.Slot) { case "fill": { if (node is TextNode) Name += "-Text"; AddFill(geometry); break; } case "stroke": Name += node is TextNode ? "-TextStroke" : "-Border"; AddStrokeColor(geometry); break; } break; case StyleType.TEXT when node is TextNode text: AddSharedTextStyle(text.style); break; case StyleType.EFFECT when node is IBlendMixin blend: AddBlend(blend); break; case StyleType.GRID or StyleType.NONE: LogWarningIgnoredFigmaProperty(node, $"{nameof(styleSlot)} {nameof(styleSlot.styleType)} with {styleSlot.styleType}"); break; default: throw new NotSupportedException(); } } public UssStyle(string name, AssetsInfo assetsInfo, BaseNode node) : this(name, assetsInfo) { if (node.IsSvgNode()) { AddSvg(assetsInfo, node); // AddSvg has to be called first in the constructor, because it overwrites its own boundingbox } else { if (node is IGeometryMixin geometry) AddGeometry(geometry); AddBorderRadius(node); } if (node is IBlendMixin blend) AddBlend(blend); if (node is ILayoutMixin layout && !node.IsRootNode()) AddLayout(layout); if (node is IDefaultFrameMixin frame) AddFrame(frame); if (node is TextNode text) AddText(text); } #endregion #region Methods internal UssStyle CopyFrom(UssStyle style) { style.Attributes.ForEach(x => Attributes[x.Key] = x.Value); return this; } void AddFrame(IDefaultFrameMixin frame) { if (frame.clipsContent) overflow = Visibility.Hidden; LayoutDouble4 correctedPadding = frame.GetCorrectedPadding(); if (correctedPadding.Any()) padding = correctedPadding.ToLength4Property(); if (frame.layoutMode is LayoutMode.NONE) return; if (frame.layoutWrap is LayoutWrap.WRAP) flexWrap = FlexWrap.Wrap; if (frame.layoutMode is LayoutMode.HORIZONTAL) flexDirection = FlexDirection.Row; justifyContent = frame.primaryAxisAlignItems switch { PrimaryAxisAlignItems.MIN => JustifyContent.FlexStart, PrimaryAxisAlignItems.CENTER => JustifyContent.Center, PrimaryAxisAlignItems.MAX => JustifyContent.FlexEnd, PrimaryAxisAlignItems.SPACE_BETWEEN => JustifyContent.SpaceBetween, _ => throw new NotSupportedException() }; alignItems = frame.counterAxisAlignItems switch { CounterAxisAlignItems.MIN => Align.FlexStart, CounterAxisAlignItems.CENTER => Align.Center, CounterAxisAlignItems.MAX => Align.FlexEnd, CounterAxisAlignItems.BASELINE => Align.Center, _ => throw new NotSupportedException() }; if (frame.itemSpacing > 0.0) itemSpacing = frame.itemSpacing; } void AddGeometry(IGeometryMixin geometry) { AddFill(geometry); if (!geometry.HasBorder()) return; AddStrokeColor(geometry); if (geometry.individualStrokeWeights != null) { if (geometry.individualStrokeWeights.left > 0) borderLeftWidth = geometry.individualStrokeWeights.left; if (geometry.individualStrokeWeights.right > 0) borderRightWidth = geometry.individualStrokeWeights.right; if (geometry.individualStrokeWeights.top > 0) borderTopWidth = geometry.individualStrokeWeights.top; if (geometry.individualStrokeWeights.bottom > 0) borderBottomWidth = geometry.individualStrokeWeights.bottom; } else { borderWidth = geometry.strokeWeight; } } void AddLayout(ILayoutMixin layout) { void SetPositioning(IDefaultFrameMixin parent) { Rect frameBorderBox = layout.GetBorderBox(); Rect parentContentBox = parent.GetContentBox(); double x = frameBorderBox.x - parentContentBox.x; double y = frameBorderBox.y - parentContentBox.y; double r = parentContentBox.width - frameBorderBox.width - x; double b = parentContentBox.height - frameBorderBox.height - y; switch (layout.constraints.horizontal) { case ConstraintHorizontal.LEFT: left = x; break; case ConstraintHorizontal.RIGHT: right = r; break; case ConstraintHorizontal.LEFT_RIGHT: left = x; right = r; break; case ConstraintHorizontal.CENTER: if (parent.layoutMode is LayoutMode.VERTICAL or LayoutMode.NONE) alignSelf = Align.Center; else if (parent.primaryAxisAlignItems is not PrimaryAxisAlignItems.CENTER) LogWarningImpossibleDesign((IBaseNodeMixin)layout, $"Has center constraint on {nameof(LayoutMode.HORIZONTAL)} axis, but parent doesnt align items center it has {parent.layoutMode}."); if (parent.HasBorder() && parent.strokeAlign is not StrokeAlign.OUTSIDE) LogWarningImpossibleDesign((IBaseNodeMixin)layout, $"Center constraint should only be combined with parent that has {nameof(StrokeAlign.OUTSIDE)} border. Parent has {parent.strokeAlign}. The scaling will be off by a few pixels when growing"); double cx = parentContentBox.halfWidth - frameBorderBox.halfWidth - x; if (Math.Abs(cx) >= tolerance) translate = new Length2Property(new LengthProperty[] { -cx, 0.0 }); break; case ConstraintHorizontal.SCALE when parentContentBox.width > 0.0: left = new LengthProperty(100.0 * x / parentContentBox.width, Unit.Percent); right = new LengthProperty(100.0 * r / parentContentBox.width, Unit.Percent); break; case ConstraintHorizontal.SCALE: break; default: throw new NotSupportedException(); } switch (layout.constraints.vertical) { case ConstraintVertical.TOP: top = y; break; case ConstraintVertical.BOTTOM: bottom = b; break; case ConstraintVertical.TOP_BOTTOM: top = y; bottom = b; break; case ConstraintVertical.CENTER: if (parent.layoutMode is not LayoutMode.HORIZONTAL) alignSelf = Align.Center; else if (parent.primaryAxisAlignItems is not PrimaryAxisAlignItems.CENTER) LogWarningImpossibleDesign((IBaseNodeMixin)layout, $"Has center constraint on {nameof(LayoutMode.VERTICAL)} axis, but parent doesnt align items center it has {parent.layoutMode}"); if (parent.HasBorder() && parent.strokeAlign is not StrokeAlign.OUTSIDE) LogWarningImpossibleDesign((IBaseNodeMixin)layout, $"Center constraint should only be combined with parent that has {nameof(StrokeAlign.OUTSIDE)} border. Parent has {parent.strokeAlign}. The scaling will be off by a few pixels when growing"); double cy = parentContentBox.halfHeight - frameBorderBox.halfHeight - y; if (Math.Abs(cy) >= tolerance) translate = new Length2Property(new[] { translate[0], -cy }); break; case ConstraintVertical.SCALE when parentContentBox.height > 0.0: top = new LengthProperty(100.0 * y / parentContentBox.height, Unit.Percent); bottom = new LengthProperty(100.0 * b / parentContentBox.height, Unit.Percent); break; case ConstraintVertical.SCALE: break; default: throw new NotSupportedException(); } } bool BoxSizingCorrection(IDefaultFrameMixin parent) { // When strokesIncludedInLayout it's the same as box-sizing: border-box if (parent.strokesIncludedInLayout) return false; // We modify position of elements to emulate negative padding. However, in UI Toolkit child cant to grow bigger than its parent. LayoutDouble4 parentNegativePadding = parent.GetCorrectedPadding().OnlyNegativeValues(); bool anyHorizontal = parentNegativePadding.left < -tolerance || parentNegativePadding.right < -tolerance; bool anyVertical = parentNegativePadding.top < -tolerance || parentNegativePadding.bottom < -tolerance; bool wrongHorizontal = layout.layoutSizingHorizontal is LayoutSizing.FILL && anyHorizontal; bool wrongVertical = layout.layoutSizingVertical is LayoutSizing.FILL && anyVertical; if (wrongHorizontal || wrongVertical) { LogWarningImpossibleDesign((IBaseNodeMixin)layout, $"{nameof(LayoutSizing.FILL)} on node with child bounding box being outside parents content, Unity auto layout doesn't allow child to outgrow parents content. " + $"Wrong {(wrongHorizontal && wrongVertical ? "Horizontal and Vertical" : wrongHorizontal ? "Horizontal" : "Vertical")} axis. " + $"Element has ({layout.layoutSizingHorizontal}, {layout.layoutSizingVertical}), parent has border {parent.strokeAlign} with width {(string)parent.GetBorderWidths().ToLength4Property()}. " + "Unity wont grow child above parent. Possible fixes:" + $"\n1: Change {nameof(IDefaultFrameMixin.strokesIncludedInLayout)} in Auto layout settings to included." + $"\n2: Use {nameof(LayoutSizing.FIXED)} or {nameof(LayoutSizing.HUG)}\n3: increase padding, currently missing {(string)(-parentNegativePadding).ToLength4Property()} more padding to make them not overlap." + $"\n3: make parent border {nameof(StrokeAlign.OUTSIDE)}"); return false; } bool horizontal = parent.layoutMode is LayoutMode.HORIZONTAL; double primary = parent.primaryAxisAlignItems switch { PrimaryAxisAlignItems.MIN or PrimaryAxisAlignItems.SPACE_BETWEEN => 1.0 * (horizontal ? parentNegativePadding.left : parentNegativePadding.top), PrimaryAxisAlignItems.MAX => -1.0 * (horizontal ? parentNegativePadding.right : parentNegativePadding.bottom), PrimaryAxisAlignItems.CENTER => 0.5 * (horizontal ? parentNegativePadding.left - parentNegativePadding.right : parentNegativePadding.top - parentNegativePadding.bottom), _ => 0.0 }; double counter = parent.counterAxisAlignItems switch { CounterAxisAlignItems.MIN => 1.0 * (horizontal ? parentNegativePadding.top : parentNegativePadding.left), CounterAxisAlignItems.MAX => -1.0 * (horizontal ? parentNegativePadding.bottom : parentNegativePadding.right), CounterAxisAlignItems.CENTER => 0.5 * (horizontal ? parentNegativePadding.top - parentNegativePadding.bottom : parentNegativePadding.left - parentNegativePadding.right), _ => 0.0 }; (double leftCorrection, double topCorrection) = horizontal ? (primary, counter) : (counter, primary); if (Math.Abs(topCorrection) > tolerance) top = topCorrection; if (Math.Abs(leftCorrection) > tolerance) left = leftCorrection; return true; } IDefaultFrameMixin parent = (IDefaultFrameMixin)((IBaseNodeMixin)layout).parent; bool forceAutoHorizontal = false; bool forceAutoVertical = false; if (parent.layoutMode is LayoutMode.NONE || layout.layoutPositioning is LayoutPositioning.ABSOLUTE) { forceAutoHorizontal = layout.constraints.horizontal is ConstraintHorizontal.SCALE or ConstraintHorizontal.LEFT_RIGHT; forceAutoVertical = layout.constraints.vertical is ConstraintVertical.SCALE or ConstraintVertical.TOP_BOTTOM; position = Position.Absolute; SetPositioning(parent); } else { LayoutDouble4 margin4 = new(); if (BoxSizingCorrection(parent)) margin4 -= (layout as IGeometryMixin).GetOutsideBorderWidths(); if (Math.Abs(parent.itemSpacing) > tolerance && parent.primaryAxisAlignItems is not PrimaryAxisAlignItems.SPACE_BETWEEN && layout != parent.children.LastOrDefault(x => x.IsVisible() && x is not ILayoutMixin { layoutPositioning: LayoutPositioning.ABSOLUTE })) { if (parent.layoutMode is LayoutMode.HORIZONTAL) margin4.right += parent.itemSpacing; else margin4.bottom += parent.itemSpacing; } if (margin4.Any()) margin = margin4.ToLength4Property(); LayoutSizing primarySizing = parent.layoutMode is LayoutMode.HORIZONTAL ? layout.layoutSizingHorizontal : layout.layoutSizingVertical; if (primarySizing is LayoutSizing.FIXED or LayoutSizing.HUG) flexShrink = 0.0; // Shrink clamps child to parent. Figma ignores this when using fixed or hug. } if (layout.layoutGrow > 0) flexGrow = 1; if (layout.layoutAlign is LayoutAlign.STRETCH) alignSelf = Align.Stretch; Rect borderBox = layout.GetBorderBox(); // Unity uses border-box if (layout.layoutSizingHorizontal is LayoutSizing.FIXED && !forceAutoHorizontal) width = borderBox.width; if (layout.layoutSizingVertical is LayoutSizing.FIXED && !forceAutoVertical) height = borderBox.height; if (layout.minWidth != null) minWidth = layout.minWidth.Value; if (layout.minHeight != null) minHeight = layout.minHeight.Value; if (layout.maxWidth != null) maxWidth = layout.maxWidth.Value; if (layout.maxHeight != null) maxHeight = layout.maxHeight.Value; const double rad2deg = 180.0 / Math.PI; if (Math.Abs(layout.rotation) > tolerance && !((IBaseNodeMixin)layout).IsSvgNode()) { LogWarningImpossibleDesign(layout as IBaseNodeMixin, "Rotation and anchors are different in figma and unity. Its best to remove rotation from figma elements. In Figma rotation is applied and then constraints. In unity anchors are applied first. Meaning a full screen figma 1920x1080 becomes a 1080x1920 in unity."); rotate = new LengthProperty(layout.rotation * rad2deg, Unit.Degrees); } if (layout is TextNode { style: { textAutoResize: TextAutoResize.WIDTH_AND_HEIGHT } } text) { if (parent.counterAxisAlignItems is CounterAxisAlignItems.BASELINE) // Baseline is not supported by unity, so we emulate it { double maxSize = parent.children.OfType().Max(x => x.style.fontSize); const double fontSizeToOffset = 1.0 / 2.75; // Approximate value gotten through trail and error. This works when parent is CounterAxisCenter, siblings have same font family and height is set to auto. Otherwise its random if it works or not double baselineOffset = (maxSize - text.style.fontSize) * fontSizeToOffset; if (Math.Abs(baselineOffset) > tolerance) top = baselineOffset; } else { const double idealLineHeightFactor = 1.2; height = text.style.lineHeightPx; // Unity text only aligns correctly when height is 1.2 times font size. Figma allows any text height if (text.style.lineHeightPx <= text.style.fontSize * idealLineHeightFactor) // Figma centers text when lineheight is too small text.style.textAlignVertical = TextAlignVertical.CENTER; } } } void AddText(TextNode text) { TextNode.Style style = text.style; string horizontal = style.textAlignHorizontal switch { TextAlignHorizontal.LEFT => nameof(left), TextAlignHorizontal.RIGHT => nameof(right), TextAlignHorizontal.CENTER => "center", TextAlignHorizontal.JUSTIFIED => throw new NotSupportedException(), _ => throw new NotSupportedException() }; string vertical = style.textAlignVertical switch { TextAlignVertical.TOP => "upper", TextAlignVertical.BOTTOM => "lower", TextAlignVertical.CENTER => "middle", _ => throw new NotSupportedException() }; unityTextAlign = (EnumProperty)$"{vertical}-{horizontal}"; if (style.textTruncation is TextTruncation.ENDING) { textOverflow = TextOverflow.Ellipsis; overflow = Visibility.Hidden; } switch (style.textAutoResize) { case TextAutoResize.NONE: case TextAutoResize.HEIGHT when style.maxLines is not 1: whiteSpace = Wrap.Normal; break; case TextAutoResize.TRUNCATE: LogWarningIgnoredFigmaProperty(text, $"{nameof(TextAutoResize)}.{nameof(TextAutoResize.TRUNCATE)} is deprecated, should be changed for element in figma document to use either {nameof(TextAutoResize.HEIGHT)} or {nameof(TextAutoResize.WIDTH_AND_HEIGHT)}"); break; } AddSharedTextStyle(style); } void AddBlend(IBlendMixin blend) { if (blend.opacity < 1.0 - tolerance) opacity = UssStyleExtension.AlphaCorrection(blend.opacity); IEnumerable effects = blend.effects.OfType().Where(x => x.visible); ShadowEffect effect = effects.FirstOrDefault(); if (effect == null) return; if (effects.Count() > 1) LogWarningIgnoredFigmaProperty((IBaseNodeMixin)blend, $"More than 1 effects, we support 1 effect per element, this has {effects.Count()}"); ShadowProperty shadow = new(effect.offset.x, effect.offset.y, effect.radius, effect.color); if (blend is TextNode text) { if (effect.type is EffectType.DROP_SHADOW) textShadow = shadow; else LogWarningImpossibleDesign(text, $"Cant use {effect.type} for text we can only use {nameof(EffectType.DROP_SHADOW)}"); } else { boxShadow = shadow; LogWarningIgnoredFigmaProperty((IBaseNodeMixin)blend, "Effects on elements, we support effects on text only"); } } void AddBorderRadius(BaseNode node) { // Unity makes each corner as round as possible while figma limits borders to max 50% of size. In Figma when a corner is bigger than the rest that corner can make other corner less round, we dont know how to recreate that effect in unity // Figma makes outside borders have radius equal to radius + border double maxBorderRadius = node is ILayoutMixin layout ? Math.Min(layout.GetBorderBox().width, layout.GetBorderBox().height) * 0.5 : double.MaxValue; LayoutDouble4 outsideBorder = node is IGeometryMixin geometry ? geometry.GetOutsideBorderWidths() : new LayoutDouble4(); if (node is ICornerMixin { cornerRadius: > 0 } corner) { LayoutDouble4 rad = outsideBorder + new LayoutDouble4(corner.cornerRadius.Value); LayoutDouble4 radius = new(Math.Min(maxBorderRadius, rad.top), Math.Min(maxBorderRadius, rad.left), Math.Min(maxBorderRadius, rad.bottom), Math.Min(maxBorderRadius, rad.right)); borderRadius = radius.ToLength4Property(); } else if (node is IRectangleCornerMixin { rectangleCornerRadii: not null } rectangleCorner && rectangleCorner.rectangleCornerRadii.Any(x => x > 0.0)) { double GetBorderCorrection(double radius, double border) => radius == 0.0 ? 0.0 : Math.Min(maxBorderRadius, radius + border); double[] radii = rectangleCorner.rectangleCornerRadii; LayoutDouble4 radius = new(GetBorderCorrection(radii[0], outsideBorder.top), GetBorderCorrection(radii[1], outsideBorder.right), GetBorderCorrection(radii[2], outsideBorder.bottom), GetBorderCorrection(radii[3], outsideBorder.left)); borderRadius = radius.ToLength4Property(); } } void AddSvg(AssetsInfo assetsInfo, BaseNode svg) { IGeometryMixin geometry = (IGeometryMixin)svg; ILayoutMixin layout = (ILayoutMixin)svg; Rect boundingBox = layout.absoluteBoundingBox; if (geometry.HasBorder()) { boundingBox.x -= geometry.strokeWeight / 2.0; if (geometry.strokeCap is not StrokeCap.NONE) boundingBox.y -= geometry.strokeWeight / 2.0; } layout.absoluteBoundingBox = boundingBox; string extension = svg.HasImage() ? KnownFormats.png : KnownFormats.svg; if (assetsInfo.GetAssetPath(svg.id, extension, out string url)) backgroundImage = Url(url); } #endregion #region Support Methods void AddSharedTextStyle(TextNode.Style style) { const string defaultFont = "Inter-Regular"; bool TryGetFontWithExtension(string font, out string resource) { if (assetsInfo.GetAssetPath(font, KnownFormats.ttf, out string ttfPath)) { resource = Url(ttfPath); return true; } if (assetsInfo.GetAssetPath(font, KnownFormats.otf, out string otfPath)) { resource = Url(otfPath); return true; } resource = Resource(defaultFont); return false; } (string, string) GetFont() { if (string.IsNullOrEmpty(style.fontPostScriptName) && string.IsNullOrEmpty(style.fontFamily)) return (null, null); string fontPostScriptName = style.fontPostScriptName ?? $"{style.fontFamily}-{style.fontStyle.Replace(" ", string.Empty)}"; string weightPostfix; if (style.fontWeight > 0) weightPostfix = Enum.GetValues(typeof(FontWeight)).GetValue((int)(style.fontWeight / 100) - 1).ToString(); else weightPostfix = fontPostScriptName.Contains('-') ? fontPostScriptName.Split('-')[1].Replace(nameof(Index), string.Empty) : string.Empty; string italicPostfix = style.italic || fontPostScriptName.Contains(nameof(FontStyle.Italic)) ? nameof(FontStyle.Italic) : string.Empty; string fontName = $"{style.fontFamily}-{weightPostfix}{italicPostfix}"; if (!TryGetFontWithExtension(fontName, out string font) && !TryGetFontWithExtension(fontPostScriptName, out font)) Debug.LogWarning(Extensions.BuildTargetMessage("Cannot find Font", fontName, string.Empty)); bool exists = assetsInfo.GetAssetPath(fontName, KnownFormats.asset, out string path); return (font, exists ? path : null); } fontSize = style.fontSize; (string font, string fontDefinition) = GetFont(); if (font != null) unityFont = font; if (fontDefinition != null) unityFontDefinition = Url(fontDefinition); if (font == null && fontDefinition == null) { Debug.LogWarning($"Font style {style.fontFamily} {style.fontStyle} {style.fontPostScriptName} is not resolved"); unityFont = Url(defaultFont); } } void AddFill(IGeometryMixin geometry) { bool urlExists = false; string url = string.Empty; RGBA finalColor = new RGBA(); foreach (Paint fill in geometry.fills.Where(x => x.visible).Reverse()) { switch (fill) { case SolidPaint solid: solid.color.a = solid.opacity; finalColor = finalColor.BlendWith(solid.color); break; case GradientPaint gradient: if (geometry is TextNode) { RGBA average = gradient.gradientStops.Select(stop => stop.color).GetAverageColor(); average.a = gradient.opacity; finalColor = finalColor.BlendWith(average); } else { urlExists = assetsInfo.GetAssetPath(gradient.GetHash(), KnownFormats.svg, out url); } break; case ImagePaint image: if (geometry is TextNode text) { LogWarningImpossibleDesign(text, $"{nameof(TextElement)} with images, unity places images in background, while figma puts them inside the text"); break; } urlExists = assetsInfo.GetAssetPath(image.imageRef, KnownFormats.png, out url); unityBackgroundSize = image.scaleMode switch { ScaleMode.FILL => BackgroundSizeType.Cover, ScaleMode.FIT => BackgroundSizeType.Contain, _ => unityBackgroundSize }; break; } finalColor.a = UssStyleExtension.AlphaCorrection(finalColor.a); if (geometry is TextNode) color = new ColorProperty(finalColor); else backgroundColor = new ColorProperty(finalColor); if (urlExists) backgroundImage = Url(url); } } void AddStrokeColor(IGeometryMixin geometry) { if (geometry is TextNode) { LogWarningIgnoredFigmaProperty((IBaseNodeMixin)geometry, "Stroke on text"); // Stroke on text might not be possible at all as unity makes inside stroke and figma outside stroke return; } RGBA finalColor = new(); foreach (Paint stroke in geometry.strokes.Where(x => x.visible).Reverse()) { RGBA color = stroke switch { SolidPaint solid => solid.color, GradientPaint gradient => gradient.gradientStops.Select(stop => stop.color).GetAverageColor(), _ => throw new NotSupportedException() }; color.a = stroke.opacity; finalColor = finalColor.BlendWith(color); } finalColor.a = UssStyleExtension.AlphaCorrection(finalColor.a); borderColor = finalColor; } internal static List MakeTransitionStyles(UssStyle root, UssStyle idle, UssStyle hover = null, UssStyle active = null) { List transitions = new() { new UssStyle(root.Name) { Target = idle, opacity = 1 } }; if (hover != null) transitions.Add(new UssStyle(root.Name) { Target = idle, PseudoClass = PseudoClass.Hover, opacity = 0 }); if (active != null) transitions.Add(new UssStyle(root.Name) { Target = idle, PseudoClass = PseudoClass.Active, opacity = 0 }); if (hover != null) { transitions.Add(new UssStyle(root.Name) { Target = hover, opacity = 0 }); transitions.Add(new UssStyle(root.Name) { Target = hover, PseudoClass = PseudoClass.Hover, opacity = 1 }); if (active != null) transitions.Add(new UssStyle(root.Name) { Target = hover, PseudoClass = PseudoClass.Active, opacity = 0 }); } if (active != null) { transitions.Add(new UssStyle(root.Name) { Target = active, opacity = 0 }); if (hover != null) transitions.Add(new UssStyle(root.Name) { Target = active, PseudoClass = PseudoClass.Hover, opacity = 0 }); transitions.Add(new UssStyle(root.Name) { Target = active, PseudoClass = PseudoClass.Active, opacity = 1 }); } return transitions; } static void LogWarningIgnoredFigmaProperty(IBaseNodeMixin node, string message) => Debug.LogWarning(Extensions.BuildTargetMessage("Ignored figma property", node.GetFullPath(), $"No support for {message}. Not implemented.")); static void LogWarningImpossibleDesign(IBaseNodeMixin node, string message) => Debug.LogWarning(Extensions.BuildTargetMessage("Wrong figma design", node.GetFullPath(), $"{message}. We cant create this in Unity.")); #endregion } } ================================================ FILE: Editor/Core/Uss/UssStyle.cs.meta ================================================ fileFormatVersion: 2 guid: 8d258033e1644b1fa80fcdf1fb668783 timeCreated: 1727945365 ================================================ FILE: Editor/Core/Uss/UssWriter.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Figma.Core.Uss { using Internals; internal class UssWriter : IDisposable, IAsyncDisposable { #region Fields readonly string rootDirectory; readonly StreamWriter stream; int count; #endregion #region Constructors public UssWriter(string rootDirectory, string path) { this.rootDirectory = rootDirectory; stream = new StreamWriter(Path = path, false, Encoding.UTF8, 1024); } #endregion #region Properties public string Path { get; } #endregion #region Methods public void Write(BaseUssStyle style) { if (!style.HasAttributes) return; if (count > 0) { stream.WriteLine(); stream.WriteLine(); } stream.Write(style.BuildName()); stream.WriteLine(" {"); foreach ((string key, string value) in style.Attributes) { switch (key) { case "background-image" when value.Contains("url"): // Getting a relative path to image to keep the reference. // Since images are located in a parent of a parent directory. // Assets/Images directory and frames are in Assets/Frames/CanvasName/ // We need to calculate a relative path and the only way of doing this // is on write operation, since UssStyle is not aware of the path, where it // would be written. stream.WriteLine($"\t{key}: url('{System.IO.Path.GetRelativePath(System.IO.Path.GetDirectoryName(Path), PathExtensions.CombinePath(rootDirectory, value[5..^2]))?.Replace('\\', '/')}');"); continue; default: stream.WriteLine($"\t{key}: {value};"); break; } } stream.Write("}"); count++; Write(style.SubStyles); } public void Write(IEnumerable styles) => styles.OrderBy(x => x.Name).ForEach(Write); void IDisposable.Dispose() => stream?.Dispose(); async ValueTask IAsyncDisposable.DisposeAsync() { if (stream != null) await stream.DisposeAsync(); } #endregion } } ================================================ FILE: Editor/Core/Uss/UssWriter.cs.meta ================================================ fileFormatVersion: 2 guid: d732fb08b4e246c6bce62bad8551f8f8 timeCreated: 1727945385 ================================================ FILE: Editor/Core/Uss.meta ================================================ fileFormatVersion: 2 guid: eb2949d8c99346768cecb2a0968b2029 timeCreated: 1732192985 ================================================ FILE: Editor/Core/Uxml/UxmlBuilder.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityAsyncAwaitUtil; using UnityEngine; namespace Figma.Core.Uxml { using Internals; using static Const; using static Internals.Extensions; using static Internals.PathExtensions; internal class UxmlBuilder { #region Fields readonly Data data; readonly NodeMetadata nodeMetadata; readonly string globalUssFilePath; readonly StylesPreprocessor stylesPreprocessor; #endregion #region Constructors public UxmlBuilder(Data data, NodeMetadata nodeMetadata, string globalUssFilePath, StylesPreprocessor stylesPreprocessor) { this.data = data; this.nodeMetadata = nodeMetadata; this.globalUssFilePath = globalUssFilePath; this.stylesPreprocessor = stylesPreprocessor; } #endregion #region Methods public void CreateDocument(string directory, string fileName, DocumentNode documentNode, IReadOnlyDictionary> framesPaths) { string CreateTemplateName(string path) => RemoveExtension(path).Replace('/', '-'); using UxmlWriter writer = new(directory, fileName); writer.WriteUssStyleReference(GetRelativePath(writer.filePath, globalUssFilePath)); writer.StartElement(documentNode, stylesPreprocessor.GetClassList(documentNode), nodeMetadata.GetElementType(documentNode)); foreach (string templatePath in framesPaths.SelectMany(framesPath => framesPath.Value)) { string path = GetRelativePath(writer.filePath, templatePath); writer.WriteTemplate(CreateTemplateName(path), path); } foreach ((string name, IReadOnlyList scope) in framesPaths) { if (scope.Count == 0) continue; writer.StartElement(nameof(UnityEngine.UIElements.VisualElement), ("picking-mode", "ignore"), ("class", "unity-viewport"), (nameof(name), name)); foreach (string path in scope) { string frameName = Path.GetFileNameWithoutExtension(path); string templateName = CreateTemplateName(GetRelativePath(writer.filePath, path)); writer.WriteInstance(frameName, templateName, Uss.UssStyle.viewportClass.Name); } writer.EndElement(); } writer.EndElement(); } public string CreateFrame(string directory, string[] ussStyleFilesPath, IReadOnlyDictionary templates, DefaultFrameNode frameNode) { using UxmlWriter writer = new(directory, frameNode.name); WriteStyles(ussStyleFilesPath, writer); foreach ((string templateName, string templatePath) in templates) writer.WriteTemplate(templateName, GetRelativePath(writer.filePath, templatePath)); WriteNodesRecursively(frameNode, writer); return writer.filePath; } public string CreateComponentSet(string directory, string[] ussStyleFilesPath, ComponentSetNode componentSetNode) { using UxmlWriter writer = new(directory, componentSetNode.name); WriteStyles(ussStyleFilesPath, writer); WriteNodesRecursively(componentSetNode, writer); return writer.filePath; } public string CreateElement(string directory, string[] ussStyleFilesPath, DefaultShapeNode node, string template) { (bool isHash, string hashedTemplate) = nodeMetadata.GetTemplate(node); using UxmlWriter writer = new(directory, isHash ? hashedTemplate : template); WriteStyles(ussStyleFilesPath, writer); if (node is DefaultFrameNode parent) foreach (SceneNode child in parent.children) WriteNodesRecursively(child, writer); return writer.filePath; } void WriteNodesRecursively(BaseNode node, UxmlWriter uxml, bool isComponent = false) { void WriteCanvasNode(CanvasNode canvasNode, UxmlWriter writer) { writer.StartElement(canvasNode, stylesPreprocessor.GetClassList(canvasNode), nodeMetadata.GetElementType(canvasNode)); canvasNode.children.ForEach(child => WriteNodesRecursively(child, writer)); writer.EndElement(); } void WriteSectionNode(SectionNode sectionNode, UxmlWriter writer) { writer.StartElement(sectionNode, stylesPreprocessor.GetClassList(sectionNode), nodeMetadata.GetElementType(sectionNode)); sectionNode.children.ForEach(child => WriteNodesRecursively(child, writer)); writer.EndElement(); throw new NotImplementedException(nameof(WriteSectionNode)); } void WriteSliceNode(SliceNode sliceNode, UxmlWriter writer) { writer.StartElement(sliceNode, stylesPreprocessor.GetClassList(sliceNode), nodeMetadata.GetElementType(sliceNode)); writer.EndElement(); } void WriteTextNode(TextNode textNode, UxmlWriter writer) { writer.StartElement(textNode, stylesPreprocessor.GetClassList(textNode), nodeMetadata.GetElementType(textNode)); string text = textNode.style.textCase switch { TextCase.UPPER => textNode.characters.ToUpper(Culture), TextCase.LOWER => textNode.characters.ToLower(Culture), _ => textNode.characters }; writer.xmlWriter.WriteAttributeString(nameof(text), text); writer.EndElement(); } void WriteDefaultFrameNode(DefaultFrameNode defaultFrameNode, UxmlWriter writer) { WriteDefaultShapeNode(defaultFrameNode, writer, false); defaultFrameNode.children.ForEach(child => WriteNodesRecursively(child, writer, isComponent)); writer.EndElement(); } void WriteBooleanOperationNode(BooleanOperationNode node, UxmlWriter writer) { WriteDefaultShapeNode(node, writer); // We shouldn't process children for vector boolean operation } void WriteDefaultShapeNode(DefaultShapeNode defaultShapeNode, UxmlWriter writer, bool closeElement = true) { string tooltip = null; if (nodeMetadata.GetTemplate(defaultShapeNode) is (var hash, { } template) && template.NotNullOrEmpty()) tooltip = hash ? template : null; writer.StartElement(defaultShapeNode, stylesPreprocessor.GetClassList(defaultShapeNode), nodeMetadata.GetElementType(defaultShapeNode)); if (tooltip.NotNullOrEmpty()) writer.xmlWriter.WriteAttributeString(nameof(tooltip), tooltip!); // Use tooltip as a storage for hash template name if (closeElement) writer.EndElement(); } void WriteInstanceNode(InstanceNode instanceNode, UxmlWriter writer) { if (data.components.TryGetValue(instanceNode.componentId, out Component component) && !component.remote && !string.IsNullOrEmpty(component.componentSetId) && data.componentSets.TryGetValue(component.componentSetId, out Component target) && !target.remote) { string componentSetName = target.name; string classList = stylesPreprocessor.GetClassList(instanceNode); writer.WriteInstance(instanceNode.name, componentSetName, classList); } else { // Since this code only runs from Parallel, outside of Unity scope // We cannot use Debug.Log() without returning to Unity's thread SyncContextUtil.UnitySynchronizationContext.Post(_ => Debug.LogWarning(BuildTargetMessage($"Target {nameof(Component)} for node", instanceNode.GetFullPath(), "is not found")), null); WriteDefaultFrameNode(instanceNode, writer); } } if (!node.IsVisible() || (!nodeMetadata.EnabledInHierarchy(node) && node is not ComponentSetNode && !isComponent)) return; if (node is CanvasNode canvas) WriteCanvasNode(canvas, uxml); if (node is FrameNode frame) WriteDefaultFrameNode(frame, uxml); if (node is GroupNode group) WriteDefaultFrameNode(group, uxml); if (node is ComponentSetNode componentSet) WriteDefaultFrameNode(componentSet, uxml); if (node is SliceNode slice) WriteSliceNode(slice, uxml); if (node is RectangleNode rectangle) WriteDefaultShapeNode(rectangle, uxml); if (node is LineNode line) WriteDefaultShapeNode(line, uxml); if (node is EllipseNode ellipse) WriteDefaultShapeNode(ellipse, uxml); if (node is RegularPolygonNode regularPolygon) WriteDefaultShapeNode(regularPolygon, uxml); if (node is StarNode star) WriteDefaultShapeNode(star, uxml); if (node is VectorNode vector) WriteDefaultShapeNode(vector, uxml); if (node is TextNode text) WriteTextNode(text, uxml); if (node is ComponentNode component) WriteDefaultFrameNode(component, uxml); if (node is InstanceNode instance) WriteInstanceNode(instance, uxml); if (node is BooleanOperationNode booleanOperation) WriteBooleanOperationNode(booleanOperation, uxml); if (node is SectionNode sectionNode) WriteDefaultFrameNode(sectionNode, uxml); // WriteSectionNode(sectionNode, uxml); } #endregion #region Support Methods void WriteStyles(string[] styles, UxmlWriter writer) => styles.ForEach(ussPath => writer.WriteUssStyleReference(CombinePath(GetRelativePath(writer.filePath, ussPath)))); #endregion } } ================================================ FILE: Editor/Core/Uxml/UxmlBuilder.cs.meta ================================================ fileFormatVersion: 2 guid: 501228cf0ce84dfda7160110c165f212 timeCreated: 1727945409 ================================================ FILE: Editor/Core/Uxml/UxmlWriter.cs ================================================ using System; using System.Text; using System.Xml; using UnityEngine.UIElements; namespace Figma.Core.Uxml { using Internals; using static Const; using static Internals.Const; using static Internals.PathExtensions; internal sealed class UxmlWriter : IDisposable { const string elementsNamespace = "ui"; static readonly XmlWriterSettings xmlWriterSettings = new() { OmitXmlDeclaration = true, Indent = true, IndentChars = indentCharacters, NewLineOnAttributes = false, Encoding = Encoding.UTF8 }; #region Fields internal readonly string filePath; internal readonly XmlWriter xmlWriter; #endregion #region Constructors public UxmlWriter(string directory, string fileName) { filePath = CombinePath(directory, $"{fileName}.{KnownFormats.uxml}"); xmlWriter = XmlWriter.Create(filePath, xmlWriterSettings); xmlWriter.WriteStartElement(elementsNamespace, "UXML", uxmlNamespace); } #endregion #region Methods public void Dispose() { xmlWriter.WriteEndElement(); xmlWriter?.Dispose(); } public void StartElement(BaseNode node, string ussClasses, (ElementType type, string typeFullName) elementTypeInfo) { (string prefix, string elementName, string pickingMode) GetElementData(BaseNode node) { string prefix = elementsNamespace; string elementName = nameof(VisualElement); PickingMode pickingMode = PickingMode.Ignore; if (elementTypeInfo.type == ElementType.IElement) { prefix = null; elementName = elementTypeInfo.typeFullName; pickingMode = PickingMode.Position; } else if (elementTypeInfo.type == ElementType.None) { if (node is not (DefaultFrameNode or TextNode or ComponentSetNode)) return (prefix, elementName, pickingMode.ToString()); const string inputsPrefix = "Inputs"; const string buttonsPrefix = "Buttons"; const string togglesPrefix = "Toggles"; const string scrollViewsPrefix = "ScrollViews"; if (node is TextNode) elementName = node.name.StartsWith(inputsPrefix) ? nameof(TextField) : nameof(Label); if (node.name.StartsWith(buttonsPrefix)) elementName = nameof(Button); else if (node.name.StartsWith(togglesPrefix)) elementName = nameof(Toggle); else if (node.name.StartsWith(scrollViewsPrefix)) elementName = nameof(ScrollView); pickingMode = node.name.StartsWith(buttonsPrefix) || node.name.StartsWith(togglesPrefix) || node.name.StartsWith(scrollViewsPrefix) || (node is TextNode && node.name.StartsWith(inputsPrefix)) ? PickingMode.Position : pickingMode; } else { elementName = elementTypeInfo.type.ToString(); pickingMode = elementTypeInfo.type is ElementType.VisualElement or ElementType.BindableElement or ElementType.Box or ElementType.TextElement or ElementType.Label or ElementType.Image ? PickingMode.Ignore : PickingMode.Position; } return (prefix, elementName, pickingMode.ToString()); } (string prefix, string elementName, string pickingMode) = GetElementData(node); if (prefix.NotNullOrEmpty()) xmlWriter.WriteStartElement(prefix, elementName, uxmlNamespace); else xmlWriter.WriteStartElement(elementName); xmlWriter.WriteAttributeString("name", node.name); xmlWriter.WriteAttributeString("id", node.id); if (ussClasses.NotNullOrEmpty()) xmlWriter.WriteAttributeString("class", ussClasses); if (pickingMode != nameof(PickingMode.Position)) xmlWriter.WriteAttributeString("picking-mode", pickingMode); } public void StartElement(string type, params (string name, string value)[] attributes) { xmlWriter.WriteStartElement(elementsNamespace, type, uxmlNamespace); foreach ((string name, string value) attribute in attributes) xmlWriter.WriteAttributeString(attribute.name, attribute.value); } public void EndElement() => xmlWriter.WriteEndElement(); public void WriteUssStyleReference(string path) { StartElement(nameof(Style), ("src", path)); EndElement(); } public void WriteTemplate(string templateName, string templatePath) { StartElement("Template", ("name", templateName), ("src", templatePath)); EndElement(); } public void WriteInstance(string instanceName, string templateName, string classList = null) { StartElement("Instance", ("name", instanceName), ("template", templateName), ("picking-mode", "ignore")); if (!string.IsNullOrEmpty(classList)) xmlWriter.WriteAttributeString("class", classList); EndElement(); } #endregion } } ================================================ FILE: Editor/Core/Uxml/UxmlWriter.cs.meta ================================================ fileFormatVersion: 2 guid: 32914bba2eaf4c938f1c10a15860d6fa timeCreated: 1733218152 ================================================ FILE: Editor/Core/Uxml.meta ================================================ fileFormatVersion: 2 guid: b264c8b3ae0846dc87a6a96ccb5c5dbf timeCreated: 1733218052 ================================================ FILE: Editor/Core.meta ================================================ fileFormatVersion: 2 guid: 7101b854596dd674dad786dffc6936ab folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Extensions/Extensions.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace Figma { using Internals; [DebuggerStepThrough] internal static class Extensions { #region Const static readonly string[] unitsMap = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" }; static readonly string[] tensMap = { "zero", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety" }; #endregion #region Methods internal static int ToBit(this bool value) => value ? 1 : 0; internal static IEnumerable IndexRedundantNames(this IReadOnlyList items, Func getName, Action setName, Func postfixConverter) { foreach (IGrouping group in items.GroupBy(getName).Where(y => y.Count() > 1)) { int i = 0; foreach (T item in group) setName(item, postfixConverter(i++)); } return items; } internal static string NumberToWords(this int number) { switch (number) { case 0: return unitsMap[0]; case < 0: return $"minus-{NumberToWords(Math.Abs(number))}"; } string words = string.Empty; if (number / 1000000 > 0) { words += $"{NumberToWords(number / 1000000)}-million "; number %= 1000000; } if (number / 1000 > 0) { words += $"{NumberToWords(number / 1000)}-thousand "; number %= 1000; } if (number / 100 > 0) { words += $"{NumberToWords(number / 100)}-hundred "; number %= 100; } if (number <= 0) return words; if (words != string.Empty) words += "and-"; if (number < 20) words += unitsMap[number]; else { words += tensMap[number / 10]; if (number % 10 > 0) words += $"-{unitsMap[number % 10]}"; } return words; } internal static RGBA GetAverageColor(this IEnumerable colors) { RGBA avgColor = new(); int count = 0; foreach (RGBA color in colors) { avgColor.r += color.r; avgColor.g += color.g; avgColor.b += color.b; avgColor.a += color.a; count++; } if (count == 0) return avgColor; avgColor.r /= count; avgColor.g /= count; avgColor.b /= count; avgColor.a /= count; return avgColor; } #endregion } } ================================================ FILE: Editor/Extensions/Extensions.cs.meta ================================================ fileFormatVersion: 2 guid: 7130aa1fa17b4bffa5e38dc5887c8364 timeCreated: 1732199774 ================================================ FILE: Editor/Extensions/NodeExtensions.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; namespace Figma { using Internals; [DebuggerStepThrough] internal static class NodeExtensions { #region Methods internal static IEnumerable Flatten(this IBaseNodeMixin root, Func filter = null) { Stack nodes = new(); nodes.Push(root); if (root is DocumentNode documentNode) foreach (CanvasNode canvasNode in documentNode.children) nodes.Push(canvasNode); for (int depth = 0; depth < Const.maximumAllowedDepthLimit; depth++) { if (nodes.Count == 0) yield break; IBaseNodeMixin node = nodes.Pop(); if (filter != null && !filter(node)) continue; yield return node; if (node is IChildrenMixin parent) foreach (SceneNode child in parent.children) nodes.Push(child); } throw new InvalidOperationException(Const.maximumDepthLimitReachedExceptionMessage); } internal static bool IsRootNode(this IBaseNodeMixin node) => node is DocumentNode or CanvasNode or ComponentNode || node.parent is CanvasNode or ComponentNode; internal static bool IsSvgNode(this IBaseNodeMixin node) => node is LineNode or EllipseNode or RegularPolygonNode or StarNode or VectorNode || (node is BooleanOperationNode && node.Flatten().Any(x => x is not BooleanOperationNode && IsVisible(x) && IsSvgNode(x))); internal static bool IsVisible(this IBaseNodeMixin node) => (node is not ISceneNodeMixin scene || scene.visible) && (node.parent == null || node.parent.IsVisible()); internal static bool HasImage(this IBaseNodeMixin node) => node is IGeometryMixin geometry && geometry.fills.Any(x => x is ImagePaint); internal static void SetParent(this BaseNode node) { switch (node) { case DocumentNode document: foreach (CanvasNode canvas in document.children) { canvas.parent = node; SetParent(canvas); } break; case IChildrenMixin { children: not null } children: foreach (SceneNode child in children.children) { child.parent = node; SetParent(child); } break; } } internal static string GetHash(this GradientPaint gradient) { using SHA1CryptoServiceProvider sha1 = new(); using MemoryStream stream = new(); using BinaryWriter writer = new(stream); writer.Write((int)gradient.type); foreach (ColorStop stop in gradient.gradientStops) { writer.Write(stop.position); writer.Write(stop.color.r); writer.Write(stop.color.g); writer.Write(stop.color.b); writer.Write(stop.color.a); } foreach (Vector position in gradient.gradientHandlePositions) { writer.Write(position.x); writer.Write(position.y); } byte[] bytes = stream.ToArray(); byte[] hashBytes = sha1.ComputeHash(bytes); StringBuilder hashBuilder = new(); foreach (byte @byte in hashBytes) hashBuilder.Append(@byte.ToString("x2")); return hashBuilder.ToString(); } internal static string GetFullPath(this IBaseNodeMixin node) { string result = string.Empty; for (int depth = 0; depth < Const.maximumAllowedDepthLimit; depth++) { if (node == null) return result; result = string.IsNullOrEmpty(result) ? node.name : node.name + PathExtensions.unixPathSeperator + result; node = node.parent; } throw new InvalidOperationException(Const.maximumDepthLimitReachedExceptionMessage); } #endregion } } ================================================ FILE: Editor/Extensions/NodeExtensions.cs.meta ================================================ fileFormatVersion: 2 guid: bc624b728724707428b12ccb11dd0658 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Extensions/UssStyleExtensions.cs ================================================ using System; using System.Linq; namespace Figma { using Core.Uss; using Internals; using UnityEngine; using Rect = Internals.Rect; static class UssStyleExtension { const bool forceAlphaCorrection = true; #region Methods internal static bool HasBorder(this IGeometryMixin geometry) => geometry.strokes.Any(x => x.visible) && geometry.strokeWeight > UssStyle.tolerance; internal static LayoutDouble4 GetBorderWidths(this IGeometryMixin geometry) { if (geometry is null or TextNode || !HasBorder(geometry)) return new LayoutDouble4(); LayoutDouble4 borders = geometry.individualStrokeWeights != null ? new LayoutDouble4(geometry.individualStrokeWeights.top, geometry.individualStrokeWeights.right, geometry.individualStrokeWeights.bottom, geometry.individualStrokeWeights.left) : new LayoutDouble4(geometry.strokeWeight); return borders; } internal static LayoutDouble4 GetOutsideBorderWidths(this IGeometryMixin geometry) => geometry.GetBorderWidths() * geometry.GetOutsideFraction(); internal static LayoutDouble4 GetInsideBorderWidths(this IGeometryMixin geometry) => geometry.GetBorderWidths() * (1 - geometry.GetOutsideFraction()); internal static Rect GetContentBox(this ILayoutMixin layout) { LayoutDouble4 border = GetInsideBorderWidths(layout as IGeometryMixin); double x = layout.absoluteBoundingBox.x + border.left; double y = layout.absoluteBoundingBox.y + border.top; double width = layout.absoluteBoundingBox.width - border.left - border.right; double height = layout.absoluteBoundingBox.height - border.top - border.bottom; return new Rect(x, y, width, height); } internal static Rect GetBorderBox(this ILayoutMixin layout) { LayoutDouble4 border = GetOutsideBorderWidths(layout as IGeometryMixin); double x = layout.absoluteBoundingBox.x - border.left; double y = layout.absoluteBoundingBox.y - border.top; double width = layout.absoluteBoundingBox.width + border.left + border.right; double height = layout.absoluteBoundingBox.height + border.top + border.bottom; return new Rect(x, y, width, height); } internal static LayoutDouble4 GetCorrectedPadding(this IDefaultFrameMixin frame) => new LayoutDouble4(frame.paddingTop, frame.paddingRight, frame.paddingBottom, frame.paddingLeft) - (frame.strokesIncludedInLayout ? new LayoutDouble4() : GetInsideBorderWidths(frame)); internal static RGBA BlendWith(this RGBA foreground, RGBA background) { double blend = background.a * (1.0 - foreground.a); RGBA color = new(); color.a = foreground.a + blend; const double alphaTolerance = 0.01; if (color.a < alphaTolerance) return new RGBA(); color.r = (foreground.r * foreground.a + background.r * blend) / color.a; color.g = (foreground.g * foreground.a + background.g * blend) / color.a; color.b = (foreground.b * foreground.a + background.b * blend) / color.a; return color; } // Magical formula found by testing different methods. including LinearToGammaSpaceExact(a), 1-GammaToLinearSpaceExact(1-a), pow(a, 1/2.2), pow(a, 1/1.8), 1.0-pow(1-a, 2.2). This method yielded results close to figma with only one layer of blending. However, with multiple semitransparent elements, this alpha correction makes them darker than figma public static double AlphaCorrection(double a) => (forceAlphaCorrection || UnityEditor.PlayerSettings.colorSpace is UnityEngine.ColorSpace.Linear) && a is > 0.0 and < 1.0 ? 1.0f - Mathf.GammaToLinearSpace(1.0f - (float)a) : a; #endregion #region Support Methods static double GetOutsideFraction(this IGeometryMixin geometry) => geometry.strokeAlign switch { StrokeAlign.OUTSIDE => 1.0, StrokeAlign.CENTER => 0.5, StrokeAlign.INSIDE => 0.0, _ => throw new NotSupportedException() }; #endregion } } ================================================ FILE: Editor/Extensions/UssStyleExtensions.cs.meta ================================================ fileFormatVersion: 2 guid: cd5b7b5e37724678a84ec7eb169a0c3d timeCreated: 1744032663 ================================================ FILE: Editor/Extensions.meta ================================================ fileFormatVersion: 2 guid: 15d902d3df7e2ae40a71e11e3a45e807 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Figma.Editor.asmdef ================================================ { "name": "Figma.Editor", "rootNamespace": "Figma.Editor", "references": [ "Unity.VectorGraphics.Editor", "AsyncAwaitUtil", "CommonUtils", "Figma" ], "includePlatforms": [ "Editor" ] } ================================================ FILE: Editor/Figma.Editor.asmdef.meta ================================================ fileFormatVersion: 2 guid: 5eb4f7c69a0b05842a5bd445fe8970e8 AssemblyDefinitionImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/FigmaDownloader.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEditor; namespace Figma { using Core; using Core.Assets; using Internals; using static Internals.Const; using static Internals.PathExtensions; internal class FigmaDownloader : Api { #region Consts const int maxConcurrentRequests = 5; const int maxComponentsIdsInOneRequest = 400; #endregion #region Fields readonly AssetsInfo assetsInfo; string componentsDirectoryPath; string elementsDirectoryPath; string imagesDirectoryPath; string framesDirectoryPath; FigmaWriter figmaWriter; NodeMetadata nodeMetadata; NodesRegistry nodesRegistry; StylesPreprocessor stylesPreprocessor; #endregion #region Constructors internal FigmaDownloader(string personalAccessToken, string fileKey, AssetsInfo assetsInfo) : base(personalAccessToken, fileKey) => this.assetsInfo = assetsInfo; #endregion #region Methods internal async Task Run(bool downloadImages, string uxmlName, IReadOnlyCollection frames, bool prune, bool filter, bool systemCopyBuffer, int progress, CancellationToken token) { Directory.CreateDirectory(framesDirectoryPath = assetsInfo.GetAbsolutePath(framesDirectoryName)); Directory.CreateDirectory(imagesDirectoryPath = assetsInfo.GetAbsolutePath(imagesDirectoryName)); Directory.CreateDirectory(elementsDirectoryPath = assetsInfo.GetAbsolutePath(elementsDirectoryName)); Directory.CreateDirectory(componentsDirectoryPath = assetsInfo.GetAbsolutePath(componentsDirectoryName)); int steps = downloadImages ? 5 : 4; await assetsInfo.cachedAssets.LoadAsync(token); Progress.Report(progress, 1, steps, "Downloading file"); List visibleSceneNodes = new(32); if (filter) { Progress.SetDescription(progress, "Filtering nodes"); Data shallowData = await GetAsync($"files/{fileKey}?depth=2", token); shallowData.document.SetParent(); NodeMetadata shallowMetadata = new(shallowData.document, frames, true, false, true); visibleSceneNodes.AddRange(shallowData.document.children.SelectMany(x => x.children).Where(shallowMetadata.EnabledInHierarchy).Select(node => node.id)); Progress.SetDescription(progress, string.Empty); } string idsString = string.Empty; if (visibleSceneNodes.Any()) idsString = $"?ids={string.Join(",", visibleSceneNodes)}"; Progress.SetDescription(progress, "Resolving Figma file"); string json = await GetJsonAsync($"files/{fileKey}{idsString}", token); if (systemCopyBuffer) GUIUtility.systemCopyBuffer = json; Progress.Report(progress, 2, steps, "Parsing Figma file"); Data data = await ConvertOnBackgroundAsync(json, token); data.document.SetParent(); Progress.SetDescription(progress, "Creating entities"); nodeMetadata = new NodeMetadata(data.document, frames, filter); nodesRegistry = new NodesRegistry(data, nodeMetadata); stylesPreprocessor = new StylesPreprocessor(data, assetsInfo); figmaWriter = new FigmaWriter(assetsInfo.directory, uxmlName, data, stylesPreprocessor, nodeMetadata, assetsInfo); Progress.Report(progress, 3, steps, "Downloading missing components"); await DownloadDocumentsAsync(token); if (downloadImages) { Progress.Report(progress, 4, steps, "Downloading images"); Progress.SetDescription(progress, "Writing Gradients"); await WriteGradientsAsync(token); Progress.SetDescription(progress, "Downloading image fills"); await GetImageFillsAsync(progress, nodesRegistry.ImageFills, token); Progress.SetDescription(progress, $"Downloading {KnownFormats.png} files"); await GetImageNodesAsync(progress, nodesRegistry.Pngs, UxmlDownloadImages.RenderAsPng, KnownFormats.png, token); Progress.SetDescription(progress, $"Downloading {KnownFormats.svg} files"); await GetImageNodesAsync(progress, nodesRegistry.Svgs, UxmlDownloadImages.RenderAsSvg, KnownFormats.svg, token); await assetsInfo.cachedAssets.SaveAsync(); } Progress.SetStepLabel(progress, string.Empty); Progress.Report(progress, steps, steps, "Updating *.uss/*.uxml files"); await figmaWriter.WriteAsync(prune); } internal void CleanUp(bool cleanImages = false) { void CleanDirectory(string directory, string[] filters) { IEnumerable target = filters.SelectMany(filter => GetFiles(directory, filter, SearchOption.AllDirectories)) .Where(fileName => !assetsInfo.modifiedContent.Contains(fileName)); foreach (string file in target) { FileUtil.DeleteFileOrDirectory(file); string meta = $"{file}.{KnownFormats.meta}"; if (File.Exists(meta)) FileUtil.DeleteFileOrDirectory(meta); } } string[] textExtensions = { $"*.{KnownFormats.uxml}", $"*.{KnownFormats.uss}" }; CleanDirectory(componentsDirectoryPath, textExtensions); CleanDirectory(elementsDirectoryPath, textExtensions); CleanDirectory(framesDirectoryPath, textExtensions); if (!cleanImages) return; string[] imagesExtensions = { $"*.{KnownFormats.svg}", $"*.{KnownFormats.png}" }; CleanDirectory(imagesDirectoryPath, imagesExtensions); } public void RemoveEmptyDirectories() { void RemoveDirectoryWithMeta(string path) { FileUtil.DeleteFileOrDirectory(path); string meta = $"{path}.{KnownFormats.meta}"; if (File.Exists(meta)) FileUtil.DeleteFileOrDirectory(meta); } void RemoveEmptyDirectory(string path, bool recursive) { if (!Directory.Exists(path)) return; if (recursive) { Stack directories = new(); directories.Push(path); for (int depth = 0; depth < Const.maximumAllowedDepthLimit; depth++) { if (directories.Count == 0) return; path = directories.Pop(); if (!Directory.EnumerateFileSystemEntries(path).Any()) RemoveDirectoryWithMeta(path); else foreach (string subDirectory in Directory.EnumerateDirectories(path)) directories.Push(subDirectory); } throw new InvalidOperationException(Const.maximumDepthLimitReachedExceptionMessage); } if (Directory.EnumerateFileSystemEntries(path).Any()) return; RemoveDirectoryWithMeta(path); } RemoveEmptyDirectory(componentsDirectoryPath, false); RemoveEmptyDirectory(elementsDirectoryPath, false); RemoveEmptyDirectory(framesDirectoryPath, true); RemoveEmptyDirectory(imagesDirectoryPath, false); } #endregion #region Support Methods async Task DownloadDocumentsAsync(CancellationToken token) { async Task> GetMissingComponentsAsync(IEnumerable components) => (await Task.WhenAll(components.Chunk(maxComponentsIdsInOneRequest) .Select(chunk => GetAsync($"files/{fileKey}/nodes?ids={string.Join(",", chunk.Distinct())}", token)))) .SelectMany(node => node.nodes.Values.Where(value => value != null)); if (nodesRegistry.MissingComponents.Count > 0) foreach (Nodes.Document value in await GetMissingComponentsAsync(nodesRegistry.MissingComponents)) stylesPreprocessor.AddMissingComponent(value.document, value.styles); } async Task GetImageFillsAsync(int progress, List imageFills, CancellationToken token) { IBaseNodeMixin[] nodes = imageFills.Where(x => nodeMetadata.ShouldDownload(x, UxmlDownloadImages.ImageFills)).ToArray(); if (!nodes.Any()) return; Data.Images filesImages = await GetAsync($"files/{fileKey}/images", token); IEnumerable imageRefs = nodes.OfType().Select(y => y.fills.OfType().First().imageRef); IEnumerable> urls = filesImages.meta.images.Where(item => imageRefs.Contains(item.Key)); await urls.ForEachParallelAsync(maxConcurrentRequests, x => GetImageAsync(x.Key, x.Value, KnownFormats.png, progress, token), token); } async Task GetImageNodesAsync(int progress, IEnumerable targetNodes, UxmlDownloadImages downloadImages, string extension, CancellationToken token) { IBaseNodeMixin[] nodes = targetNodes.Where(x => nodeMetadata.ShouldDownload(x, downloadImages)).ToArray(); if (!nodes.Any()) return; int i = 0; IEnumerable> groups = nodes.Select(x => x.id).GroupBy(_ => i++ / 100); Task[] tasks = groups.Select(x => GetAsync($"images/{fileKey}?ids={string.Join(",", x)}&format={extension}", token)).ToArray(); await Task.WhenAll(tasks); IEnumerable> urls = tasks.SelectMany(x => x.Result.images); await urls.ForEachParallelAsync(maxConcurrentRequests, x => GetImageAsync(x.Key, x.Value, extension, progress, token), token); } async Task GetImageAsync(string nodeID, string url, string extension, int progress, CancellationToken token) { if (string.IsNullOrEmpty(url)) { Debug.LogWarning($"Node {nodeID} does not have any URL for image. Most likely, this node uses a mask."); return; } bool fileExists = assetsInfo.GetAssetPath(nodeID, extension, out string assetPath); if (assetsInfo.modifiedContent.Contains(assetsInfo.GetAbsolutePath(assetPath))) return; Progress.SetStepLabel(progress, url); using HttpRequestMessage request = new(HttpMethod.Get, url); if (fileExists && assetsInfo.cachedAssets.Map.TryGetValue(nodeID, out string etag)) request.Headers.Add("If-None-Match", $"\"{etag}\""); HttpResponseMessage response = await httpClient.SendAsync(request, token); bool isResolved = assetsInfo.cachedAssets.Map.ContainsValue(nodeID); if (response.Headers.TryGetValues("ETag", out IEnumerable values)) assetsInfo.cachedAssets[nodeID] = values.First().Trim('"'); assetsInfo.GetAssetPath(nodeID, extension, out assetPath); string path = assetsInfo.GetAbsolutePath(assetPath); if (assetsInfo.modifiedContent.Contains(path)) return; assetsInfo.modifiedContent.Add(path); if (response.IsSuccessStatusCode && !isResolved) { byte[] bytes = await response.Content.ReadAsByteArrayAsync(); if (bytes.Length == 0) { Debug.LogWarning($"Response is empty for node={nodeID}, url={url}"); if (extension == KnownFormats.svg) await File.WriteAllTextAsync(path, InvalidSvg, token); else await File.WriteAllBytesAsync(path, InvalidPng, token); } else { try { await File.WriteAllBytesAsync(path, bytes, token); } catch (Exception exception) { Debug.LogException(exception); } } } } async Task WriteGradientsAsync(CancellationToken token) { foreach ((string key, GradientPaint gradient) in nodesRegistry.Gradients) { assetsInfo.GetAssetPath(key, KnownFormats.svg, out string relativePath); string xmlPath = assetsInfo.GetAbsolutePath(relativePath); using GradientWriter writer = new(xmlPath); await writer.WriteAsync(gradient, token); assetsInfo.modifiedContent.Add(xmlPath); } } #endregion } } ================================================ FILE: Editor/FigmaDownloader.cs.meta ================================================ fileFormatVersion: 2 guid: 51ab4c8a6a174660b9a8bff7530d8631 timeCreated: 1695908086 ================================================ FILE: Editor/FigmaWriter.cs ================================================ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; // ReSharper disable UnusedMember.Local // ReSharper disable UnusedParameter.Local #pragma warning disable S1144 // Unused private types or members should be removed namespace Figma { using Core; using Core.Assets; using Core.Uss; using Core.Uxml; using Internals; using static Internals.Const; using static Internals.PathExtensions; internal sealed class FigmaWriter { #region Fields readonly string directory; readonly string fileName; readonly string ussPath; readonly Data data; readonly NodeMetadata nodeMetadata; readonly AssetsInfo assetsInfo; readonly StylesPreprocessor stylesPreprocessor; #endregion #region Properties DocumentNode Document => data.document; #endregion #region Constructors internal FigmaWriter(string directory, string fileName, Data data, StylesPreprocessor stylesPreprocessor, NodeMetadata nodeMetadata, AssetsInfo assetsInfo) { this.directory = directory; this.fileName = fileName; this.data = data; this.nodeMetadata = nodeMetadata; this.assetsInfo = assetsInfo; this.stylesPreprocessor = stylesPreprocessor; ussPath = CombinePath(directory, $"{fileName}.{KnownFormats.uss}"); } #endregion #region Methods internal async Task WriteAsync(bool overrideGlobal = false) { RootNodes rootNodes = new(data, nodeMetadata); // We do need this, since the Data.componentSets do not contain updated names. Dictionary componentSets = rootNodes.ComponentSets .OrderBy(x => x.id) // Ordering to avoid index confusion, since order in the Collection could vary from one request to another. .ToArray() .IndexRedundantNames(x => x.name, (componentSet, postfix) => componentSet.name += postfix, index => index == 0 ? string.Empty : "-" + index) .ToDictionary(x => x.id); KeyValuePair[] nodeStyleFiltered = stylesPreprocessor.NodeStyleMap.Where(x => x.Key.IsVisible() && (nodeMetadata.EnabledInHierarchy(x.Key) || x.Key is ComponentSetNode)).ToArray(); UssStyle[] nodeStyleStatelessFiltered = nodeStyleFiltered.Select(x => x.Value).ToArray(); UssStyle[] globalStaticStyles = stylesPreprocessor.Styles.Select(x => x.style).Where(x => nodeStyleStatelessFiltered.Any(y => y.DoesInherit(x))).ToArray(); if (overrideGlobal) { await using UssWriter globalUssWriter = new(directory, ussPath); globalUssWriter.Write(UssStyle.overrideClass); globalUssWriter.Write(UssStyle.viewportClass); globalUssWriter.Write(globalStaticStyles.ToArray().IndexRedundantNames(x => x.Name, (style, postfix) => style.Name += postfix, index => "-" + (index + 1).NumberToWords())); } // Writing UXML files UxmlBuilder uxmlBuilder = new(data, nodeMetadata, ussPath, stylesPreprocessor); Dictionary> framesPaths = new(rootNodes.Frames.Count); foreach (CanvasNode canvasNode in rootNodes.Canvases) framesPaths.Add(canvasNode.name, new List()); List tasks = new(rootNodes.Frames.Count + rootNodes.ComponentSets.Count + rootNodes.Elements.Count); tasks.AddRange(rootNodes.Frames.Select(x => Task.Run(() => WriteFrame(uxmlBuilder, framesPaths, componentSets, x)))); tasks.AddRange(rootNodes.ComponentSets.Select(x => Task.Run(() => WriteComponentSet(uxmlBuilder, x)))); tasks.AddRange(rootNodes.Elements.Select(x => Task.Run(() => WriteTemplate(uxmlBuilder, x)))); await Task.WhenAll(tasks); // Creating main UXML document if (overrideGlobal) uxmlBuilder.CreateDocument(directory, fileName, data.document, framesPaths); } #endregion #region Support Methods void WriteFrame(UxmlBuilder uxmlBuilder, Dictionary> framesPaths, Dictionary componentSets, FrameNode frameNode) { Dictionary templates = new(); void FindTemplates(BaseNode root) { Stack nodes = new(); nodes.Push(root); for (int depth = 0; depth < Const.maximumAllowedDepthLimit; depth++) { if (nodes.Count == 0) return; BaseNode node = nodes.Pop(); if (!node.IsVisible() || !nodeMetadata.EnabledInHierarchy(node)) continue; if (node is InstanceNode instanceNode) { Component component = data.components[instanceNode.componentId]; if (component == null || component.remote || string.IsNullOrEmpty(component.componentSetId)) continue; Component componentSet = data.componentSets[component.componentSetId]; if (componentSet == null || componentSet.remote) continue; string template = componentSets[component.componentSetId].name; templates[template] = CombinePath(directory, componentsDirectoryName, $"{template}.{KnownFormats.uxml}"); } else if (nodeMetadata.GetTemplate(node) is (_, { } template) && template.NotNullOrEmpty()) templates[template] = CombinePath(directory, elementsDirectoryName, $"{template}.{KnownFormats.uxml}"); if (node is DefaultFrameNode frameNode) foreach (SceneNode child in frameNode.children) nodes.Push(child); } throw new System.InvalidOperationException(Const.maximumDepthLimitReachedExceptionMessage); } string rootDirectory = CombinePath(directory, framesDirectoryName, frameNode.parent.name); if (!Directory.Exists(rootDirectory)) Directory.CreateDirectory(rootDirectory); using UssWriter ussWriter = new(directory, CombinePath(rootDirectory, $"{frameNode.name}.{KnownFormats.uss}")); ussWriter.Write(stylesPreprocessor.GetStyles(frameNode).IndexRedundantNames(x => x.Name, (style, postfix) => style.Name += postfix, index => "-" + (index + 1).NumberToWords())); FindTemplates(frameNode); string uxmlPath = uxmlBuilder.CreateFrame(rootDirectory, new[] { ussPath, ussWriter.Path }, templates, frameNode); framesPaths[frameNode.parent.name].As>().Add(uxmlPath); assetsInfo.AddModifiedFiles(uxmlPath, ussWriter.Path); templates.Clear(); } void WriteComponentSet(UxmlBuilder uxmlBuilder, ComponentSetNode componentSet) { using UssWriter ussWriter = new(directory, CombinePath(directory, componentsDirectoryName, $"{componentSet.name}.{KnownFormats.uss}")); ussWriter.Write(stylesPreprocessor.GetStyles(componentSet).IndexRedundantNames(x => x.Name, (style, postfix) => style.Name += postfix, index => "-" + (index + 1).NumberToWords())); string uxmlPath = uxmlBuilder.CreateComponentSet(CombinePath(directory, componentsDirectoryName), new[] { ussPath, ussWriter.Path }, componentSet); assetsInfo.AddModifiedFiles(uxmlPath, ussWriter.Path); } void WriteTemplate(UxmlBuilder uxmlBuilder, (DefaultShapeNode element, string template) node) { (bool isHash, string hashedTemplates) = nodeMetadata.GetTemplate(node.element); using UssWriter ussWriter = new(directory, CombinePath(directory, elementsDirectoryName, $"{(isHash ? hashedTemplates : node.template)}.{KnownFormats.uss}")); ussWriter.Write(stylesPreprocessor.GetStyles(node.element).IndexRedundantNames(x => x.Name, (style, postfix) => style.Name += postfix, index => "-" + (index + 1).NumberToWords())); string uxmlPath = uxmlBuilder.CreateElement(CombinePath(directory, elementsDirectoryName), new[] { ussPath, ussWriter.Path }, node.element, node.template); assetsInfo.AddModifiedFiles(uxmlPath, ussWriter.Path); } #endregion } } ================================================ FILE: Editor/FigmaWriter.cs.meta ================================================ fileFormatVersion: 2 guid: cbfef092900681d478f7beff22ae8f28 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Inspector/AuthTest.cs ================================================ using System.Threading; using System.Threading.Tasks; namespace Figma.Inspectors { using Internals; internal class AuthTest : Api { #region Properties public Me me { get; private set; } public bool IsAuthenticated => me != null && string.IsNullOrEmpty(me.err) && me.email.Contains("@"); #endregion #region Constructors internal AuthTest(string personalAccessToken = null) : base(personalAccessToken, null) { } #endregion #region Methods internal async Task AuthAsync() => me = await GetAsync(nameof(me), CancellationToken.None); #endregion } } ================================================ FILE: Editor/Inspector/AuthTest.cs.meta ================================================ fileFormatVersion: 2 guid: b2f4d6368ca84ba2860d8499592a9019 timeCreated: 1696830428 ================================================ FILE: Editor/Inspector/FigmaInspector.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using Debug = UnityEngine.Debug; using PackageInfo = UnityEditor.PackageManager.PackageInfo; // ReSharper disable MemberCanBeMadeStatic.Local namespace Figma.Inspectors { using Core.Assets; using Attributes; using Internals; using static Styles; [CustomEditor(typeof(Figma), true)] [SuppressMessage("Roslynator", "RCS1213:Remove unused member declaration.")] public class FigmaInspector : Editor { #region Const static readonly Regex regex = new(@"[^/\\]+$", RegexOptions.Compiled); static readonly string patKey = $"{nameof(Figma)}/{nameof(PersonalAccessToken)}"; #endregion #region Fields SerializedProperty fileKey; SerializedProperty filter; SerializedProperty reorder; SerializedProperty fontDirectories; SerializedProperty waitFrameBeforeRebuild; UIDocument document; new Figma target; Texture2D icon; Dictionary selection; bool updating; bool resolvingName; string username; int progressId; string searchBar; #endregion #region Properties static string PersonalAccessToken { get => EditorPrefs.GetString(patKey, string.Empty); set => EditorPrefs.SetString(patKey, value); } #endregion #region Methods void Awake() => icon = AssetDatabase.LoadAssetAtPath($"{PackageInfo.FindForAssembly(typeof(Figma).Assembly)?.assetPath}/Editor/Assets/icon.png"); void OnEnable() { target = (Figma)base.target; EditorGUIUtility.SetIconForObject(target, icon); fileKey = serializedObject.FindProperty(nameof(fileKey)); filter = serializedObject.FindProperty(nameof(filter)); reorder = serializedObject.FindProperty(nameof(reorder)); fontDirectories = serializedObject.FindProperty(nameof(fontDirectories)); waitFrameBeforeRebuild = serializedObject.FindProperty(nameof(waitFrameBeforeRebuild)); document = target.GetComponent(); selection = target.GetComponentsInChildren().Cast().ToDictionary(key => key, _ => true); } public override void OnInspectorGUI() { serializedObject.Update(); DrawPersonalAccessTokenGUI(); DrawAssetGUI(); DrawFramesView(); DrawProperties(); serializedObject.ApplyModifiedProperties(); } void DrawPersonalAccessTokenGUI() { // ReSharper disable once AsyncVoidMethod async void GetNameAsync(string personalAccessToken) { resolvingName = true; using AuthTest auth = new(personalAccessToken); await auth.AuthAsync(); if (!auth.IsAuthenticated) return; username = auth.me.handle; PersonalAccessToken = personalAccessToken; resolvingName = false; } using (new EditorGUILayout.HorizontalScope(GUI.skin.box)) { if (PersonalAccessToken.NotNullOrEmpty()) { if (username.NullOrEmpty() && !resolvingName) GetNameAsync(PersonalAccessToken); EditorGUILayout.LabelField(EditorGUIUtility.TrIconContent(LoggedInIcon), GUILayout.Width(20)); EditorGUILayout.LabelField("You're logged in as", GUILayout.Width(108)); EditorGUILayout.LabelField(username, EditorStyles.boldLabel); if (GUILayout.Button(new GUIContent(LogOutIcon, "Log Out"), GUILayout.Width(25), GUILayout.Height(25))) PersonalAccessToken = string.Empty; } else { EditorGUILayout.PrefixLabel("Figma PAT"); string token = EditorGUILayout.TextField(PersonalAccessToken); if (GUI.changed) GetNameAsync(token); } } if (PersonalAccessToken.NotNullOrEmpty()) return; GUIStyle richTextHelpBox = new(EditorStyles.helpBox) { richText = true }; if (GUILayout.Button(EditorGUIUtility.TrTextContentWithIcon("You have to enter your personal access token in order to update.\n\n" + "You can get your token at https://figma.com", "console.warnicon"), richTextHelpBox)) Application.OpenURL("https://www.figma.com"); } void DrawAssetGUI() { // ReSharper disable once AsyncVoidMethod async void Update(bool downloadImages, bool pickDirectory) { try { updating = true; string[] fontDirectories = new string[this.fontDirectories.arraySize]; for (int index = 0; index < this.fontDirectories.arraySize; index++) fontDirectories[index] = this.fontDirectories.GetArrayElementAtIndex(index).stringValue; Type[] frames = selection.Where(x => x.Value).Select(x => x.Key.GetType()).ToArray(); bool prune = selection.All(x => x.Value); document.visualTreeAsset = pickDirectory ? null : document.visualTreeAsset; await UpdateWithProgressAsync(document, target, frames, prune, fileKey.stringValue, downloadImages, fontDirectories, Event.current.modifiers == EventModifiers.Control); } finally { updating = false; } } using EditorGUILayout.VerticalScope __ = new(GUI.skin.box); EditorGUILayout.PropertyField(fileKey); VisualTreeAsset visualTreeAsset = document.visualTreeAsset; using (new EditorGUI.DisabledScope(true)) EditorGUILayout.ObjectField("Asset", visualTreeAsset, typeof(VisualTreeAsset), true); if (string.IsNullOrEmpty(PersonalAccessToken)) return; using (new EditorGUILayout.HorizontalScope()) { const string downloadTooltip = "Hold `Ctrl` to copy 'figma.json' into your clipboard"; if (updating) { using (new EditorGUI.DisabledScope(true)) GUILayout.Button("Updating..."); } else if (selection.Any(x => x.Value)) { bool update = GUILayout.Button(new GUIContent(nameof(Update), DocumentsOnlyIcon, downloadTooltip), GUILayout.Height(20)); bool downloadImages = GUILayout.Button(new GUIContent("Update with Images", DocumentWithImagesIcon, downloadTooltip), GUILayout.Width(184), GUILayout.Height(20)); bool resetTargetUxml = GUILayout.Button(new GUIContent(DirectoryIcon), GUILayout.Width(36)); if (resetTargetUxml && EditorUtility.DisplayDialog("Figma Updater", "Do you want to update images as well?", "Yes", "No")) downloadImages = true; if (update || downloadImages || resetTargetUxml) { Update(downloadImages, resetTargetUxml); GUIUtility.ExitGUI(); } } } if (selection.Any(x => !x.Value) && selection.Any(x => x.Value)) EditorGUILayout.HelpBox("Selection mode does not clean up unused content. In order to get rid of unused content, \"Select All\" and \"Update\"", MessageType.Warning); if (selection.Count > 0 && selection.All(x => !x.Value)) EditorGUILayout.HelpBox("Nothing is selected for Update.", MessageType.Error); } void DrawFramesView() { if (!document || !document.visualTreeAsset) { EditorGUILayout.HelpBox("'UIDocument' not found or 'SourceAsset' is not referenced.", MessageType.Error); return; } using GUILayout.VerticalScope _ = new(GUI.skin.box); searchBar = EditorGUILayout.TextField(searchBar, EditorStyles.toolbarSearchField); bool clear = selection.All(x => x.Value); using (new GUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); if (GUILayout.Button(new GUIContent($"{(clear ? "Clear" : "Select All")} ({selection.Sum(x => x.Value.ToBit())})"), GUILayout.Width(100))) foreach (MonoBehaviour frame in selection.Keys.ToArray()) selection[frame] = !clear; } foreach (MonoBehaviour frame in selection.Keys.OrderBy(x => x.GetType().GetCustomAttribute().Root)) { Type elementType = frame.GetType(); UxmlAttribute uxml = elementType.GetCustomAttribute(); if (uxml is null || (!string.IsNullOrWhiteSpace(searchBar) && !uxml.Root.ToLower().Contains(searchBar.ToLower()))) continue; using (new EditorGUILayout.HorizontalScope()) { using (new EditorGUI.DisabledGroupScope(!selection[frame])) EditorGUILayout.LabelField(new GUIContent(uxml.Root, uxml.Preserve.Any() ? $"Preserves {uxml.Preserve.Aggregate((x, y) => $"{x} {y}")}" : null), uxml.Preserve.Any() ? EditorStyles.boldLabel : EditorStyles.label, GUILayout.Width(EditorGUIUtility.labelWidth)); using (new EditorGUI.DisabledGroupScope(true)) EditorGUI.ObjectField(EditorGUILayout.GetControlRect(), frame, typeof(MonoBehaviour), false); selection[frame] = EditorGUILayout.Toggle(selection[frame], GUILayout.Width(24)); } } } void DrawProperties() { using EditorGUILayout.VerticalScope scope = new(GUI.skin.box); EditorGUILayout.PropertyField(fontDirectories, new GUIContent("Additional Fonts Directories")); EditorGUILayout.PropertyField(reorder, new GUIContent("De-root and Re-order Hierarchy")); EditorGUILayout.PropertyField(filter, new GUIContent("Filter by Path")); EditorGUILayout.PropertyField(waitFrameBeforeRebuild, new GUIContent("Wait Frame Before Rebuild")); } #endregion #region Support Methods static async Task UpdateWithProgressAsync(UIDocument document, Figma figma, IReadOnlyList frames, bool prune, string fileKey, bool downloadImages, IReadOnlyList fontDirectories, bool systemCopyBuffer) { string GetAssetPath() { if (document.visualTreeAsset) return AssetDatabase.GetAssetPath(document.visualTreeAsset); string path = EditorUtility.SaveFilePanel($"Save {nameof(VisualTreeAsset)}", Application.dataPath, document.name, KnownFormats.uxml); if (path.NotNullOrEmpty() && Path.GetFullPath(path).StartsWith(Path.GetFullPath(Application.dataPath))) return path; PackageInfo packageInfo = PackageInfo.GetAllRegisteredPackages().First(x => Path.GetFullPath(path).StartsWith(Path.GetFullPath(x.resolvedPath))); return PathExtensions.CombinePath(packageInfo.assetPath, Path.GetFullPath(path).Replace(Path.GetFullPath(packageInfo.resolvedPath), string.Empty)); } (string directory, string relativeDirectory, string product, string uxmlName) = GetDirectoryAndRelativeDirectory(GetAssetPath()); uxmlName = Path.GetFileNameWithoutExtension(uxmlName); if (directory.NullOrEmpty() || relativeDirectory.NullOrEmpty()) return; Stopwatch stopwatch = Stopwatch.StartNew(); string display = $"Figma {product}" + (downloadImages ? " (Images)" : string.Empty); int progress = Progress.Start(display, null, Progress.Options.Managed); using CancellationTokenSource cancellationToken = new(); try { Progress.RegisterCancelCallback(progress, () => { // ReSharper disable once AccessToDisposedClosure cancellationToken.Cancel(); return true; }); AssetDatabase.StartAssetEditing(); AssetsInfo info = new(directory, relativeDirectory, uxmlName, fontDirectories); using FigmaDownloader figmaDownloader = new(PersonalAccessToken, fileKey, info); try { await figmaDownloader.Run(downloadImages, uxmlName, frames, prune, figma.Filter, systemCopyBuffer, progress, cancellationToken.Token); if (prune) figmaDownloader.CleanUp(downloadImages); } finally { if (prune) figmaDownloader.RemoveEmptyDirectories(); } document.visualTreeAsset = AssetDatabase.LoadAssetAtPath(PathExtensions.CombinePath(info.relativeDirectory, $"{uxmlName}.{KnownFormats.uxml}")); stopwatch.Stop(); EditorUtility.SetDirty(document.visualTreeAsset); Debug.Log($"{display} is updated successfully in {(float)stopwatch.ElapsedMilliseconds / 1000}s"); Progress.Finish(progress); } catch (Exception exception) { Progress.Finish(progress, Progress.Status.Failed); if (!exception.Message.Contains("404") || exception is not OperationCanceledException) throw; Debug.LogException(exception); } finally { Progress.UnregisterCancelCallback(progress); stopwatch.Reset(); AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(); } } static (string directory, string relativeDirectory, string product, string name) GetDirectoryAndRelativeDirectory(string assetPath) { if (!assetPath.StartsWith("Packages")) return (Path.GetDirectoryName(assetPath), Path.GetRelativePath(Directory.GetCurrentDirectory(), Path.GetDirectoryName(assetPath)), Application.productName, regex.Match(assetPath).Value); PackageInfo packageInfo = PackageInfo.FindForAssetPath(assetPath); return (packageInfo.resolvedPath + Path.GetDirectoryName(assetPath.Replace(packageInfo.assetPath, string.Empty)), Path.GetDirectoryName(assetPath), packageInfo.displayName, regex.Match(assetPath).Value); } #endregion } } ================================================ FILE: Editor/Inspector/FigmaInspector.cs.meta ================================================ fileFormatVersion: 2 guid: 79908b73109533b4d87fced3ad4caee7 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Inspector/Styles.cs ================================================ using UnityEditor; using UnityEngine; namespace Figma.Inspectors { public static class Styles { internal static readonly string SuccessColor = EditorGUIUtility.isProSkin ? "#00ff00" : "#00aa00"; static readonly string prefix = EditorGUIUtility.isProSkin ? "d_" : string.Empty; internal static readonly Texture DirectoryIcon = EditorGUIUtility.IconContent($"{prefix}Project").image; internal static readonly Texture LoggedInIcon = EditorGUIUtility.IconContent("TestPassed").image; internal static readonly Texture LogOutIcon = EditorGUIUtility.IconContent($"{prefix}Import").image; internal static readonly Texture DocumentsOnlyIcon = EditorGUIUtility.IconContent($"{prefix}Refresh@2x").image; internal static readonly Texture DocumentWithImagesIcon = EditorGUIUtility.IconContent($"{prefix}RawImage Icon").image; } } ================================================ FILE: Editor/Inspector/Styles.cs.meta ================================================ fileFormatVersion: 2 guid: 54fcfd1c480a4ad290dcae61139be13f timeCreated: 1739275722 ================================================ FILE: Editor/Inspector.meta ================================================ fileFormatVersion: 2 guid: ee4e1dc47f503814581a191eb139e1b8 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Interface/Const.cs ================================================ namespace Figma.Internals { public static class Const { // Http API target public const string api = "https://api.figma.com/v1"; // Directories public const string fontsDirectoryName = "Fonts"; public const string framesDirectoryName = "Frames"; public const string imagesDirectoryName = nameof(Images); public const string elementsDirectoryName = "Elements"; public const string componentsDirectoryName = "Components"; // Uxml public const string uxmlNamespace = "UnityEngine.UIElements"; // Fallback written data /// /// Magenta colored image with resolution 2x2. /// public static readonly byte[] InvalidPng = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature // IHDR chunk 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x08, 0x06, 0x00, 0x00, 0x00, 0xF4, 0x78, 0x5A, 0xEE, // IDAT chunk (compressed image data) 0x00, 0x00, 0x00, 0x11, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0xC0, 0xC0, 0xF0, 0x0F, 0x04, 0x00, 0x04, 0x00, 0x01, 0xF3, 0x0D, 0x0E, 0x43, // IEND chunk 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; /// /// Warning sign in SVG. /// public const string InvalidSvg = @" "; } public static class KnownFormats { public const string png = nameof(png); public const string svg = nameof(svg); public const string ttf = nameof(ttf); public const string otf = nameof(otf); public const string asset = nameof(asset); public const string uxml = nameof(uxml); public const string uss = nameof(uss); public const string json = nameof(json); public const string meta = nameof(meta); } } ================================================ FILE: Editor/Interface/Const.cs.meta ================================================ fileFormatVersion: 2 guid: e79ce7b985a449c58c07592648c530b7 timeCreated: 1728029275 ================================================ FILE: Editor/Interface/Enums.cs ================================================ namespace Figma { enum Unit { Default, None, Initial, Auto, Pixel, Degrees, Percent } enum Align { Auto, FlexStart, FlexEnd, Center, Stretch } enum FlexDirection { Row, RowReverse, Column, ColumnReverse } enum FlexWrap { Nowrap, Wrap, WrapReverse } enum JustifyContent { FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround } enum Position { Absolute, Relative } enum Visibility { Visible, Hidden } enum OverflowClip { PaddingBox, ContentBox } enum Display { Flex, None } enum FontStyle { Normal, Italic, Bold, BoldAndItalic } enum TextAlign { UpperLeft, MiddleLeft, LowerLeft, UpperCenter, MiddleCenter, LowerCenter, UpperRight, MiddleRight, LowerRight } enum EasingFunction { EaseIn, EaseOut, EaseInAndOut, Linear, Slow, CustomSpring } enum Wrap { Normal, Nowrap, } public enum ElementType { None, //Base elements VisualElement, BindableElement, //Utilities Box, TextElement, Label, Image, IMGUIContainer, Foldout, //Templates Template, Instance, TemplateContainer, //Controls Button, RepeatButton, Toggle, Scroller, Slider, SliderInt, MinMaxSlider, EnumField, MaskField, LayerField, LayerMaskField, TagField, ProgressBar, //Text input TextField, IntegerField, LongField, FloatField, DoubleField, Vector2Field, Vector2IntField, Vector3Field, Vector3IntField, Vector4Field, RectField, RectIntField, BoundsField, BoundsIntField, //Complex widgets PropertyField, PropertyControlInt, PropertyControlLong, PropertyControlFloat, PropertyControlDouble, PropertyControlString, ColorField, CurveField, GradientField, ObjectField, //Toolbar Toolbar, ToolbarButton, ToolbarToggle, ToolbarMenu, ToolbarSearchField, ToolbarPopupSearchField, ToolbarSpacer, //Views and windows ListView, ScrollView, TreeView, PopupWindow, IElement } enum TimeUnit { Default, Millisecond, Second } enum PseudoClass { None = 0, Hover, Active, Inactive, Focus, Selected, Disabled, Enabled, Checked, Root } enum FontWeight { Thin = 100, ExtraLight = 200, Light = 300, Regular = 400, Medium = 500, SemiBold = 600, Bold = 700, ExtraBold = 800, Black = 900 } } ================================================ FILE: Editor/Interface/Enums.cs.meta ================================================ fileFormatVersion: 2 guid: 772df3471b2405947abf34d0b8abcf8f MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Interface/Figma.Enums.cs ================================================ // ReSharper disable InconsistentNaming namespace Figma.Internals { public enum EffectType { INNER_SHADOW, DROP_SHADOW, LAYER_BLUR, BACKGROUND_BLUR } public enum BlendMode { PASS_THROUGH, NORMAL, DARKEN, MULTIPLY, LINEAR_BURN, COLOR_BURN, LIGHTEN, SCREEN, LINEAR_DODGE, COLOR_DODGE, OVERLAY, SOFT_LIGHT, HARD_LIGHT, DIFFERENCE, EXCLUSION, HUE, SATURATION, COLOR, LUMINOSITY } public enum ConstraintVertical { TOP, BOTTOM, CENTER, TOP_BOTTOM, SCALE } public enum ConstraintHorizontal { LEFT, RIGHT, CENTER, LEFT_RIGHT, SCALE } public enum PaintType { SOLID, GRADIENT_LINEAR, GRADIENT_RADIAL, GRADIENT_ANGULAR, GRADIENT_DIAMOND, IMAGE, EMOJI } public enum Pattern { COLUMNS, ROWS, GRID } public enum Alignment { MIN, MAX, STRETCH, CENTER } public enum ScaleMode { FILL, FIT, TILE, STRETCH } public enum ExportSettingsConstraintsType { SCALE, WIDTH, HEIGHT } public enum Format { JPG, PNG, SVG, PDF } public enum ActionType { BACK, CLOSE, URL, NODE } public enum Navigation { NAVIGATE, SWAP, OVERLAY, CHANGE_TO } public enum TransitionType { DISSOLVE, SMART_ANIMATE, MOVE_IN, MOVE_OUT, PUSH, SLIDE_IN, SLIDE_OUT } public enum TransitionDirection { LEFT, RIGHT, TOP, BOTTOM } public enum TriggerType { ON_CLICK, ON_HOVER, ON_PRESS, DRAG, AFTER_TIMEOUT, MOUSE_ENTER, MOUSE_LEAVE, MOUSE_UP, MOUSE_DOWN, ON_KEY_DOWN, ON_KEY_UP } public enum TriggerDevice { KEYBOARD, XBOX_ONE, PS4, SWITCH_PRO, UNKNOWN_CONTROLLER } public enum EasingType { EASE_IN, EASE_OUT, EASE_IN_AND_OUT, LINEAR, SLOW, CUSTOM_SPRING } public enum LayoutAlign { CENTER, MIN, MAX, STRETCH, INHERIT } public enum StrokeCap { NONE, ROUND, SQUARE, ARROW_LINES, ARROW_EQUILATERAL, LINE_ARROW } public enum StrokeJoin { MITER, BEVEL, ROUND } public enum StrokeAlign { INSIDE, OUTSIDE, CENTER } public enum LayoutMode { NONE, HORIZONTAL, VERTICAL } public enum PrimaryAxisSizingMode { AUTO, FIXED } public enum CounterAxisSizingMode { AUTO, FIXED } public enum PrimaryAxisAlignItems { MIN, CENTER, MAX, SPACE_BETWEEN } public enum CounterAxisAlignItems { MIN, CENTER, MAX, BASELINE } public enum OverflowDirection { NONE, HORIZONTAL_SCROLLING, VERTICAL_SCROLLING, HORIZONTAL_AND_VERTICAL_SCROLLING } public enum TextCase { ORIGINAL, UPPER, LOWER, TITLE } public enum TextDecoration { NONE, UNDERLINE, STRIKETHROUGH } public enum TextAlignHorizontal { LEFT, CENTER, RIGHT, JUSTIFIED } public enum TextAlignVertical { TOP, CENTER, BOTTOM } public enum TextAutoResize { NONE, WIDTH_AND_HEIGHT, HEIGHT, TRUNCATE } public enum BooleanOperation { UNION, INTERSECT, SUBTRACT, EXCLUDE } public enum LayoutPositioning { AUTO, ABSOLUTE } public enum StyleType { FILL, TEXT, EFFECT, GRID, NONE } public enum NodeType { DOCUMENT, CANVAS, SLICE, FRAME, GROUP, COMPONENT_SET, COMPONENT, INSTANCE, BOOLEAN_OPERATION, VECTOR, STAR, LINE, ELLIPSE, REGULAR_POLYGON, RECTANGLE, TEXT, SECTION } public enum ComponentPropertyType { BOOLEAN, TEXT, INSTANCE_SWAP, VARIANT } public enum LayoutWrap { NO_WRAP, WRAP } public enum MaskType { ALPHA, VECTOR, LUMINANCE } public enum LayoutSizing { FIXED, HUG, FILL } public enum CounterAxisAlignContent { AUTO, SPACE_BETWEEN } public enum TextTruncation { DISABLED, ENDING } public enum LineType { NONE, ORDERED, UNORDERED } public enum TextWeight { BOLD, NORMAL } public enum TextItalic { ITALIC, NORMAL } } ================================================ FILE: Editor/Interface/Figma.Enums.cs.meta ================================================ fileFormatVersion: 2 guid: 04cc6de496b9b534d9dbdcfae4f2353e MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Interface/Figma.Types.Interface.cs ================================================ using System.Collections.Generic; using Newtonsoft.Json; // ReSharper disable InconsistentNaming // ReSharper disable BuiltInTypeReferenceStyle // ReSharper disable UnusedMember.Global // ReSharper disable CollectionNeverUpdated.Global // ReSharper disable UnassignedField.Global // ReSharper disable FieldCanBeMadeReadOnly.Global #pragma warning disable S101, S4004 namespace Figma.Internals { public interface IBaseNodeMixin { NodeType type { get; set; } string id { get; set; } [JsonIgnore] BaseNode parent { get; set; } string name { get; set; } } public interface ISceneNodeMixin { bool visible { get; set; } } public interface IChildrenMixin { SceneNode[] children { get; set; } } public interface ILayoutMixin { Constraints constraints { get; set; } LayoutAlign layoutAlign { get; set; } double layoutGrow { get; set; } LayoutPositioning layoutPositioning { get; set; } LayoutSizing layoutSizingHorizontal { get; set; } LayoutSizing layoutSizingVertical { get; set; } Rect absoluteBoundingBox { get; set; } double rotation { get; set; } double[][] relativeTransform { get; set; } Vector? size { get; set; } double? minWidth { get; set; } double? minHeight { get; set; } double? maxWidth { get; set; } double? maxHeight { get; set; } } public interface IBlendMixin { double opacity { get; set; } BlendMode blendMode { get; set; } bool isMask { get; set; } Effect[] effects { get; set; } Dictionary styles { get; set; } bool preserveRatio { get; set; } } public interface IGeometryMixin { Paint[] fills { get; set; } object[] fillGeometry { get; set; } Paint[] strokes { get; set; } double strokeWeight { get; set; } StrokeAlign strokeAlign { get; set; } StrokeCap strokeCap { get; set; } StrokeJoin strokeJoin { get; set; } object[] strokeGeometry { get; set; } IndividualStrokeWeights individualStrokeWeights { get; set; } } public interface ICornerMixin { double? cornerRadius { get; set; } } public interface IRectangleCornerMixin { double[] rectangleCornerRadii { get; set; } } public interface IExportMixin { ExportSettings[] exportSettings { get; set; } } public interface IReactionMixin { Reaction[] reactions { get; set; } } public interface ITransitionMixin { string transitionNodeID { get; set; } double? transitionDuration { get; set; } EasingType? transitionEasing { get; set; } } public interface IDefaultShapeMixin : IBaseNodeMixin, ISceneNodeMixin, ILayoutMixin, IBlendMixin, IGeometryMixin, IReactionMixin, IExportMixin { } public interface IDefaultFrameMixin : IDefaultShapeMixin, ICornerMixin, IRectangleCornerMixin, IChildrenMixin { LayoutMode layoutMode { get; set; } PrimaryAxisSizingMode primaryAxisSizingMode { get; set; } PrimaryAxisAlignItems primaryAxisAlignItems { get; set; } CounterAxisSizingMode counterAxisSizingMode { get; set; } CounterAxisAlignItems counterAxisAlignItems { get; set; } CounterAxisAlignContent counterAxisAlignContent { get; set; } double paddingLeft { get; set; } double paddingTop { get; set; } double paddingRight { get; set; } double paddingBottom { get; set; } double itemSpacing { get; set; } LayoutGrid[] layoutGrids { get; set; } bool clipsContent { get; set; } OverflowDirection overflowDirection { get; set; } LayoutWrap layoutWrap { get; set; } bool itemReverseZIndex { get; set; } bool strokesIncludedInLayout { get; set; } } } ================================================ FILE: Editor/Interface/Figma.Types.Interface.cs.meta ================================================ fileFormatVersion: 2 guid: d433f8c0bc1b474487e41e90ae1ffbd3 timeCreated: 1728645091 ================================================ FILE: Editor/Interface/Figma.Types.Structs.cs ================================================ namespace Figma.Internals { public struct Vector { public double x; public double y; } public struct Rect { public double x; public double y; public double width; public double height; public double left => x; public double right => x + width; public double top => y; public double bottom => y + height; public double centerLeft => x - width / 2; public double centerRight => x + width / 2; public double centerTop => y - height / 2; public double centerBottom => y + height / 2; public double halfWidth => width / 2; public double halfHeight => height / 2; public Rect(double x, double y, double width, double height) { this.x = x; this.y = y; this.width = width; this.height = height; } public static Rect operator +(Rect a, Rect b) => new(a.x + b.x, a.y + b.y, a.width + b.width, a.height + b.height); public static Rect operator -(Rect a, Rect b) => new(a.x - b.x, a.y - b.y, a.width - b.width, a.height - b.height); } public struct RGBA { public double r; public double g; public double b; public double a; public static explicit operator UnityEngine.Color(RGBA color) => new((float) color.r, (float) color.g, (float) color.b, (float) color.a); } } ================================================ FILE: Editor/Interface/Figma.Types.Structs.cs.meta ================================================ fileFormatVersion: 2 guid: a20508fa20ef43dfb924e62827afe07d timeCreated: 1729243214 ================================================ FILE: Editor/Interface/Figma.Types.cs ================================================ using System; using System.Collections.Generic; // ReSharper disable InconsistentNaming // ReSharper disable BuiltInTypeReferenceStyle // ReSharper disable UnusedMember.Global // ReSharper disable CollectionNeverUpdated.Global // ReSharper disable UnassignedField.Global // ReSharper disable FieldCanBeMadeReadOnly.Global #pragma warning disable S101, S4004 namespace Figma.Internals { using VariableAlias = Object; #region Datatype public class ShadowEffect : Effect { public EffectType type; public RGBA color; public Vector offset; public double radius; public double? spread; public bool visible; public BlendMode blendMode; public bool? showShadowBehindNode; } public class BlurEffect : Effect { public EffectType type; public double radius; public bool visible; } public class Effect { } public class IndividualStrokeWeights { public double top; public double right; public double bottom; public double left; } public class Constraints { public ConstraintHorizontal horizontal; public ConstraintVertical vertical; } public class ColorStop { public double position; public RGBA color; } public class ImageFilter { public double? exposure; public double? contrast; public double? saturation; public double? temperature; public double? tint; public double? highlights; public double? shadows; } public class SolidPaint : Paint { public PaintType type; public RGBA color; public Dictionary boundVariables; } public class GradientPaint : Paint { public PaintType type; public ColorStop[] gradientStops; public Vector[] gradientHandlePositions; } public class ImagePaint : Paint { public PaintType type; public ScaleMode scaleMode; public double[,] imageTransform; public string imageRef; public ImageFilter filters; public double? rotation; } public class Paint { public bool visible = true; public double opacity = 1.0; public BlendMode blendMode; } public class RowsColsLayoutGrid : LayoutGrid { public Pattern pattern; public Alignment alignment; public double gutterSize; public double count; public double? sectionSize; public double? offset; public bool? visible; public RGBA? color; } public class GridLayoutGrid : LayoutGrid { public Pattern pattern; public double sectionSize; public bool? visible; public RGBA? color; public Alignment? alignment; public double? gutterSize; public double count; public double? offset; } public class LayoutGrid { } public class ExportSettingsConstraints { public ExportSettingsConstraintsType type; public double value; } public class ExportSettingsImage : ExportSettings { public Format format; public bool? contentsOnly; public string suffix; public ExportSettingsConstraints constraint; } public class ExportSettingsSVG : ExportSettings { public Format format = Format.SVG; public bool? contentsOnly; public string suffix; public bool? svgOutlineText; public bool? svgIdAttribute; public bool? svgSimplifyStroke; public ExportSettingsConstraints constraint; } public class ExportSettingsPDF : ExportSettings { public Format format = Format.PDF; public bool? contentsOnly; public string suffix; public ExportSettingsConstraints constraint; } public class ExportSettings { } public class Reaction { public Action action; public Trigger trigger; } public class Action { public ActionType type; public string url; public string destinationId; public Navigation? navigation; public Transition transition; public bool? preserveScrollPosition; public Vector? overlayRelativePosition; public bool resetVideoPosition; public bool resetScrollPosition; public bool resetInteractiveComponents; } public class SimpleTransition : Transition { } public class DirectionalTransition : Transition { public TransitionDirection direction; public bool matchLayers; } public class Transition { public TransitionType type; public Easing easing; public double? duration; } public class Trigger { public TriggerType type; public double? delay; public TriggerDevice? device; public double[] keyCodes; public double? timeout; public double? mediaHitTime; } public class Easing { public EasingType type; public EasingFunctionSpring easingFunctionSpring; } public class DocumentationLink { public string uri; } public class ArcData { public double startingAngle; public double endingAngle; public double innerRadius; } public class FlowStartingPoint { public string nodeId; public string name; } public class ComponentPropertyReferences { public string visible; public string characters; public string mainComponent; } #endregion #region Nodes public class DocumentNode : BaseNode { public CanvasNode[] children; } public class CanvasNode : BaseNode, IChildrenMixin, IExportMixin { public RGBA backgroundColor; public string prototypeStartNodeID; public object prototypeDevice; public FlowStartingPoint[] flowStartingPoints; #region Mixin public SceneNode[] children { get; set; } public ExportSettings[] exportSettings { get; set; } #endregion } public class FrameNode : DefaultFrameNode { } public class GroupNode : DefaultFrameNode { } public class SliceNode : SceneNode, ILayoutMixin, IExportMixin { #region Mixin public Constraints constraints { get; set; } public LayoutAlign layoutAlign { get; set; } public double layoutGrow { get; set; } public LayoutPositioning layoutPositioning { get; set; } public LayoutSizing layoutSizingHorizontal { get; set; } public LayoutSizing layoutSizingVertical { get; set; } public Rect absoluteBoundingBox { get; set; } public double rotation { get; set; } public Rect absoluteRenderBounds { get; set; } public double[][] relativeTransform { get; set; } public Vector? size { get; set; } public double? minWidth { get; set; } public double? minHeight { get; set; } public double? maxWidth { get; set; } public double? maxHeight { get; set; } public ExportSettings[] exportSettings { get; set; } #endregion } public class RectangleNode : DefaultShapeNode, ICornerMixin, IRectangleCornerMixin { #region Mixin public double? cornerRadius { get; set; } public double[] rectangleCornerRadii { get; set; } #endregion } public class LineNode : DefaultShapeNode { } public class EllipseNode : DefaultShapeNode { public ArcData arcData { get; set; } } public class RegularPolygonNode : DefaultShapeNode, ICornerMixin, IRectangleCornerMixin { #region Mixin public double? cornerRadius { get; set; } public double[] rectangleCornerRadii { get; set; } #endregion } public class StarNode : DefaultShapeNode, ICornerMixin, IRectangleCornerMixin { #region Mixin public double? cornerRadius { get; set; } public double[] rectangleCornerRadii { get; set; } #endregion } public class VectorNode : DefaultShapeNode, ICornerMixin, IRectangleCornerMixin { #region Mixin public double? cornerRadius { get; set; } public double[] rectangleCornerRadii { get; set; } public Dictionary fillOverrideTable { get; set; } #endregion } public class TextNode : DefaultShapeNode { public class Style { public string fontFamily; // not null public string fontPostScriptName; // can be null public double paragraphSpacing; public bool italic; public double fontWeight; public double fontSize; public TextCase textCase; public TextDecoration textDecoration; public TextAlignHorizontal textAlignHorizontal; public TextAlignVertical textAlignVertical; public TextAutoResize textAutoResize; public double letterSpacing; public Paint[] fills; public Dictionary opentypeFlags; public double lineHeightPx; public double? listSpacing; public double? lineHeightPercentFontSize; public string lineHeightUnit; public Dictionary hyperlink; public string inheritFillStyleId; public string inheritTextStyleId; public string leadingTrim; public TextTruncation textTruncation; public int maxLines; public string fontStyle; public TextWeight? semanticWeight; public TextItalic? semanticItalic; } public string characters; public Style style; public int[] characterStyleOverrides; public Dictionary styleOverrideTable; public double? layoutVersion; public LineType[] lineTypes; public int[] lineIndentations; } public class ComponentSetNode : DefaultFrameNode { public Dictionary componentPropertyDefinitions; } public class ComponentNode : DefaultFrameNode { public Dictionary componentPropertyDefinitions; } public class ComponentPropertyDefinition { public string type; public string defaultValue; public List variantOptions; public object boundVariables; public PreferredValue[] preferredValues; } public class ComponentProperties : ComponentPropertyDefinition { public string value; } public class PreferredValue { public NodeType type; public string key; } public class InstanceNode : DefaultFrameNode { public string componentId; public Dictionary componentProperties; public bool isExposedInstance; public string[] exposedInstances; public Overrides[] overrides; } public class Overrides { public string id; public string[] overriddenFields; } public class BooleanOperationNode : DefaultFrameNode { public BooleanOperation booleanOperation; } public class DefaultShapeNode : SceneNode, IDefaultShapeMixin, ITransitionMixin { public double[] strokeDashes; public Rect absoluteRenderBounds; #region Mixin public LayoutSizing layoutSizingHorizontal { get; set; } public LayoutSizing layoutSizingVertical { get; set; } public Constraints constraints { get; set; } public LayoutAlign layoutAlign { get; set; } public double layoutGrow { get; set; } public Rect absoluteBoundingBox { get; set; } public double rotation { get; set; } // only if geometry=paths public double[][] relativeTransform { get; set; } public Vector? size { get; set; } public double opacity { get; set; } = 1.0; public BlendMode blendMode { get; set; } public bool isMask { get; set; } public Effect[] effects { get; set; } public Dictionary styles { get; set; } public bool preserveRatio { get; set; } public Paint[] fills { get; set; } public object[] fillGeometry { get; set; } public Paint[] strokes { get; set; } public double strokeWeight { get; set; } public IndividualStrokeWeights individualStrokeWeights { get; set; } public StrokeAlign strokeAlign { get; set; } public StrokeCap strokeCap { get; set; } public StrokeJoin strokeJoin { get; set; } public object[] strokeGeometry { get; set; } public double strokeMiterAngle { get; set; } = 28.96; public double cornerSmoothing { get; set; } public Reaction[] reactions { get; set; } public ExportSettings[] exportSettings { get; set; } public string transitionNodeID { get; set; } public double? transitionDuration { get; set; } public EasingType? transitionEasing { get; set; } public Interactions[] interactions { get; set; } public ComponentPropertyReferences componentPropertyReferences { get; set; } public LayoutPositioning layoutPositioning { get; set; } public MaskType? maskType { get; set; } public double? minWidth { get; set; } public double? minHeight { get; set; } public double? maxWidth { get; set; } public double? maxHeight { get; set; } #endregion } public class DefaultFrameNode : DefaultShapeNode, IDefaultFrameMixin { #region Mixin public LayoutMode layoutMode { get; set; } public PrimaryAxisSizingMode primaryAxisSizingMode { get; set; } public CounterAxisSizingMode counterAxisSizingMode { get; set; } public PrimaryAxisAlignItems primaryAxisAlignItems { get; set; } public CounterAxisAlignItems counterAxisAlignItems { get; set; } public CounterAxisAlignContent counterAxisAlignContent { get; set; } public double paddingLeft { get; set; } public double paddingTop { get; set; } public double paddingRight { get; set; } public double paddingBottom { get; set; } public double itemSpacing { get; set; } public LayoutGrid[] layoutGrids { get; set; } public bool clipsContent { get; set; } public OverflowDirection overflowDirection { get; set; } public LayoutWrap layoutWrap { get; set; } public bool itemReverseZIndex { get; set; } public bool strokesIncludedInLayout { get; set; } public double? cornerRadius { get; set; } public double[] rectangleCornerRadii { get; set; } public SceneNode[] children { get; set; } #endregion } public class BaseNode : IBaseNodeMixin { #region Mixin public NodeType type { get; set; } public string id { get; set; } public BaseNode parent { get; set; } public string name { get; set; } public string scrollBehavior { get; set; } #endregion public override string ToString() => name; } public class SceneNode : BaseNode, ISceneNodeMixin { #region Properties public bool visible { get; set; } = true; public Dictionary boundVariables { get; set; } #endregion } public class SectionNode : DefaultFrameNode, IChildrenMixin { #region Properties public bool sectionContentsHidden { get; set; } #endregion } #endregion #region Api public class Component { public string key; public string name; public string description; public DocumentationLink[] documentationLinks; public string componentSetId; public bool remote; } public class Style { public StyleType styleType; public string key; public string name; public string description; public string remote; } public class Failure { public int status; public string err; } public class Me : Failure { public string id; public string email; public string handle; public string img_url; } public class Data : Failure { public class Images { public class Meta { public Dictionary images; } public bool error; public double status; public Meta meta; public string i18n; } public DocumentNode document; public Dictionary components; public Dictionary componentSets; public double schemaVersion; public Dictionary styles; public string name; public DateTime lastModified; public string thumbnailUrl; public string version; public string role; public string editorType; public string linkAccess; } public class Images : Failure { public Dictionary images; } public class Nodes : Failure { public class Document { public ComponentNode document; public Dictionary components; public Dictionary componentSets; public double schemaVersion; public Dictionary styles; } public string name; public DateTime lastModified; public string thumbnailUrl; public string version; public string role; public Dictionary nodes; public string editorType; public string linkAccess; } public class Interactions { public Trigger trigger; public List actions; } public class EasingFunctionSpring { public double? mass; public double? stiffness; public double? damping; } #endregion } ================================================ FILE: Editor/Interface/Figma.Types.cs.meta ================================================ fileFormatVersion: 2 guid: f5f4947df342cf540b908c2dd2e2a2ee MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor/Interface/Interface.Records.cs ================================================ using System; namespace Figma { using Attributes; record RootMetadata(bool filter, UxmlAttribute uxml, UxmlDownloadImages downloadImages); // ReSharper disable once NotAccessedPositionalProperty.Global record QueryMetadata(Type fieldType, QueryAttribute query); record BaseNodeMetadata(RootMetadata root, QueryMetadata query); } ================================================ FILE: Editor/Interface/Interface.Records.cs.meta ================================================ fileFormatVersion: 2 guid: a02ced439c1547dea0ba054f881f0892 timeCreated: 1727941971 ================================================ FILE: Editor/Interface.meta ================================================ fileFormatVersion: 2 guid: 9d9c7aa8bf559004a898cba5d982b661 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Editor.meta ================================================ fileFormatVersion: 2 guid: 047c80ffe3f35194089fa29faafd00ff folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: License.md ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 4. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 5. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 6. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 7. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 8. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2023 Trackman A/S Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: License.md.meta ================================================ fileFormatVersion: 2 guid: fdd2e558d847d528a87531ad64ba1e48 TextScriptImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Prefabs/Figma.prefab ================================================ %YAML 1.1 %TAG !u! tag:unity3d.com,2011: --- !u!1 &2916098490094507199 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - component: {fileID: 2492470867148571057} - component: {fileID: 6641805578009137364} - component: {fileID: -3369783722120399799} m_Layer: 0 m_Name: Figma m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 --- !u!4 &2492470867148571057 Transform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2916098490094507199} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &6641805578009137364 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2916098490094507199} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0} m_Name: m_EditorClassIdentifier: m_PanelSettings: {fileID: 11400000, guid: f486055d8ec653edc96c3f3c38380c8f, type: 2} m_ParentUI: {fileID: 0} sourceAsset: {fileID: 0} m_SortingOrder: 0 --- !u!114 &-3369783722120399799 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2916098490094507199} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: aaa4e5d0dd7bf25488b120aa854832eb, type: 3} m_Name: m_EditorClassIdentifier: title: filter: 0 reorder: 0 fontsDirs: [] ================================================ FILE: Prefabs/Figma.prefab.meta ================================================ fileFormatVersion: 2 guid: a1decc1df2e53b24285e24bfd721a22c PrefabImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Prefabs.meta ================================================ fileFormatVersion: 2 guid: c4567610e8b246a43ac8ab4dce22b73b folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Readme.md ================================================ > [!WARNING] > **Experimental Release**: This plugin is currently in an experimental phase and is provided "as is" without warranty of any kind. It was originally developed for internal use and may contain issues or limitations. Use it at your own risk. Feedback and contributions are welcome but please keep in mind the experimental nature of this tool. [output.webm](https://github.com/TrackMan/Unity.Package.FigmaForUnity/assets/22183046/59423710-61af-44b0-b233-47216881c051) # Overview FigmaToUnity is a Unity plugin that streamlines the UI development process by enabling the direct import of Figma page documents into Unity. The tool automatically converts Figma document into UI Toolkit assets, allowing for quick and accurate integration of UI interfaces into your Unity games. # Features | Name | Description | |---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | **Figma Import to UXML/USS** | The tool imports and parses Figma page documents, transforming them into UXML and USS assets within Unity. | | **Element Manipulation** | Enables the manipulation of UI elements and the application of custom logic via Unity scripts, providing extensive control over the user interface. | | **Sync Changes** | Changes made to UI elements in Figma can be readily fetched and updated in Unity, maintaining the integrity of the game's UI. | | **[Limitations](#limitations)** | Refer to the section below. | # Installing 1. Open `Window > Package Manager` 2. Click the `+` button in the top-left corner 3. Choose `Add package from git URL...` 4. Enter https://github.com/TrackMan/Unity.Package.AsyncAwaitUtil.git 5. Click the `+` button in the top-left corner 6. Click the `+` button in the top-left corner 7. Choose `Add package from git URL...` 8. Enter https://github.com/TrackMan/Unity.Package.FigmaForUnity.git ## Dependencies To integrate these dependencies, you must either manually include them in your project's manifest file or ensure they are automatically resolved through Unity's Package Manager registry. - [Async Await Util 1.0.6](https://github.com/TrackMan/Unity.Package.AsyncAwaitUtil) ## Personal Access Token To start using [Figma Inspector](#Figma-Inspector), a Figma Personal Access Token is needed for API calls. > [!WARNING] > The token is stored in raw format. 1. Visit the [Figma API Authentication Page](https://www.figma.com/developers/api?fuid=797042793200923967#authentication). 2. Click + Get personal access token to generate the token. 3. Copy the generated token. 4. Locate the Figma script in [Unity's Inspector](#Figma-Inspector). 5. Paste the token into the designated field. # Quick Start - Finish [Installing](#Installing) - Open `~Samples` folder - Open `Test.unity` - Go to `Figma` GameObject - In the `Title`, enter the title of your Figma document (ie dfeQabSU71CHXVqweameSF) from Figma website ([some templates](https://www.figma.com/community/files)) - Go to `Test.cs` and edit the UXML attribute to points to your Page/Element path - Configure [Personal Access Token](#Personal-Access-Token) in [Unity's Inspector](#Figma-Inspector) - Click `Update UI & Images` - Save the `VisualAssetTree` somewhere - Start `playmode` and enjoy! # Usage Working with this plugin is done through using Figma component inspector FigmaInspector. In addition, components derived from the Element class should be created and added to the same GameObject hierarchy. These components serve a dual purpose: 1. During the import phase, they assist in filtering and configuring various aspects of the document, such as frame selection and image handling. 2. During runtime, they provide the functionality to manipulate UXML and USS data structures. ## Figma Inspector ![image](https://user-images.githubusercontent.com/22183046/270277550-bd127c6a-1e0f-4494-8b2b-cc9e87ed0448.png) | Property | Description | |------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Panel Settings** | A crucial asset enabling Unity to render UXML-based user interfaces within the Game View. | | **Source Asset** | The UXML asset responsible for outlining the structural framework of the user interface. | | **Sort Order** | Specifies the rendering sequence of UXML assets when multiple Panel Settings instances exist within a project. | | **Title** | Designates the title from the Figma document URL for identification purposes. | | **Asset** | Represents the UXML asset correlating with the corresponding asset within the UI document script. (Refer to the section above for further details) | | **Update Buttons** | Facilitates UI updates, offering options to include or exclude the downloading of all associated images. | | **De-root and Re-order Hierarchy** | Adjusts the organization of all frames based on each element's RootOrder property, optimizing the UI hierarchy. | | **Filter by Path** | When activated, this feature limits the download to only those UI frames that have associated scripts attached to the prefab, otherwise, all UI elements within the Figma document will be downloaded. | | **Additional Fonts Directories** | Provides the capability to specify paths to any fonts that are incorporated within the UI, ensuring seamless visual consistency. | ## Folders Layout This is how the final setup should look: ``` ├── UI.uxml ├── UI.uss ├── CachedAssets_UI.json ├── Elements │ ├── CustomElement.uxml │ └── CustomElement.uss ├── Components │ ├── Component.uxml │ └── Component.uss ├── Frames │ └── Canvas1 │ ├── Frame1.uxml │ ├── Frame1.uss │ ├── Frame2.uxml │ └── Frame2.uss ├── Images │ ├── AmazingImage.svg │ └── PerfectImage.png └── Fonts └── Inter-Regular.ttf ``` Please note that you need to place your **custom fonts** in a **'Fonts' folder**. If you try to import a Figma document that includes a font which does not exist in the 'Fonts' folder, you will see a message in the console indicating which font is missing. You can also add an additional fonts directory using the appropriate property in the Figma object. ## Figma class During the update process the Figma class retrieves data based on the Uxml and Query attributes of the Element scripts. It then utilizes this data while producing UXML asset. > [!WARNING] > Element scripts should be attached to the same game object to which the Figma script is also attached. ## Element class (OnInitialize, OnRebuild, Custom Elements) The Uxml and Query attributes define the structure of the Uxml asset and, consequently, the appearance of your UI. The type of the field following the Query attribute defines the UI element itself and, consequently, its behavior (VisualElement, Button, Label, etc.). Field types are written into UXML. Therefore, if you change the field type (for example, from VisualElement to Button), you will need to perform a Figma Update to regenerate the UXML. Each element can override OnInitialize and OnRebuild methods which can be used to do any initial setup operations. ``` csharp [Uxml("TestPage/TestFrame", UxmlDownloadImages.Everything, UxmlElementTypeIdentification.ByElementType)] [AddComponentMenu("Figma/Samples/Test")] public class Test : Element { const int minCircles = 1; const int maxCircles = 7; #region Fields [Query("Header")] Label header; [Query("CloneButton", Clicked = nameof(Clone))] Button cloneButton; [Query("RemoveButton", Clicked = nameof(Remove))] Button removeButton; [Query("CloneContainer", StartRoot = true)] VisualElement cloneContainer; [Query("CloneCircle", EndRoot = true)] PerfectCircle cloneCircle; [Query("SyncButton", Clicked = nameof(Sync))] Button syncButton; [Query("SyncContainer")] VisualElement syncContainer; [Query("SyncContainer/SyncCircle")] PerfectCircle syncCircle; [Query("FunctionDescription", Hide = true)] Label functionDescription; #endregion #region Methods protected override void OnInitialize() => cloneContainer.style.flexWrap = Wrap.NoWrap; protected override void OnRebuild() => header.text = "Welcome to Figma Test Frame!"; void Clone() { if (cloneContainer.childCount == maxCircles) return; cloneCircle.Clone(cloneContainer); } void Remove() { if (cloneContainer.childCount == minCircles) return; cloneContainer.Remove(cloneContainer.Children().First()); } void Sync() { void RandomColor(int index) => syncContainer.Children().ElementAt(index).style.backgroundColor = Random.ColorHSV(); syncCircle.Sync(syncContainer, RandomColor, Enumerable.Range(0, Random.Range(1, maxCircles + 1))); syncCircle.Hide(); functionDescription.Show(); } #endregion } ``` ### Custom element example ``` csharp public class PerfectCircle : SyncButtonSimple { public new class UxmlFactory : UxmlFactory { } #region Methods public override bool IsVisible(int index, int data) => true; #endregion } ``` ## UxmlAttribute This attribute specifies the Element (a frame, and it's children) that we want to import from Figma document. | Attribute | Description | |------------------------|----------------------------------------------------------------------------------------------------------------------------------------| | **Root** | Defines the root path within the Figma document where this frame originates, inclusive of the canvas path. | | **DownloadImages** | Specifies the strategy for downloading images from the Figma document. | | **TypeIdentification** | Indicates the method for identifying the types of elements, whether based on their name or their classification under Element classes. | | **Preserve** | Lists any additional paths that should be maintained as-is in the imported document. | ## QueryAttribute This attribute specifies the sub element (inside of the Element) parameters (like path to element, or what should happen when you click button). | Event Name | Description | |---------------------------------|--------------------------------------------------------------------------| | **Path** | The path used in the UI query. | | **ClassName** | The class name of the element. | | **ImageFiltering** | Enum specifying how images are downloaded or filtered. | | **ReplaceNodePath** | Path to the node that will be replaced. | | **ReplaceNodeEvent** | Event that triggers the node replacement. | | **ReplaceElementPath** | Path to the element that will be replaced. | | **RebuildElementEvent** | Event that triggers the element to be rebuilt. | | **StartRoot** | Specifies that element path will be new root for the following elements. | | **EndRoot** | Specifies the end of StartRoot. | | **Nullable** | Specifies if the element can be null. | | **Clicked** | Name of the method to be invoked when the element is clicked. | | **Template** | Template to be used for the element (creates a separate uxml file). | | **Hash** | Create a template file using Hash. | | **UseTrickleDown** | Specifies if events should trickle down through the element hierarchy. | | **ChangeEvent** | Event triggered when an element's state changes. | | **MouseCaptureOutEvent** | Event triggered when mouse capture is lost. | | **ValidateCommandEvent** | Event triggered to validate a command. | | **ExecuteCommandEvent** | Event triggered to execute a command. | | **DragExitedEvent** | Event triggered when a drag operation exits the element. | | **DragUpdatedEvent** | Event triggered when a drag operation is updated. | | **DragPerformEvent** | Event triggered when a drag operation is performed. | | **DragEnterEvent** | Event triggered when a drag operation enters the element. | | **DragLeaveEvent** | Event triggered when a drag operation leaves the element. | | **FocusOutEvent** | Event triggered when the element loses focus. | | **BlurEvent** | Event triggered when the element is blurred. | | **FocusInEvent** | Event triggered when the element gains focus. | | **FocusEvent** | Event triggered when the element is focused or loses focus. | | **InputEvent** | Event triggered when the element receives input. | | **KeyDownEvent** | Event triggered when a key is pressed down. | | **KeyUpEvent** | Event triggered when a key is released. | | **GeometryChangedEvent** | Event triggered when the element's geometry changes. | | **PointerDownEvent** | Event triggered when a pointer is pressed down. | | **PointerUpEvent** | Event triggered when a pointer is released. | | **PointerMoveEvent** | Event triggered when a pointer is moved. | | **MouseDownEvent** | Event triggered when a mouse button is pressed. | | **MouseUpEvent** | Event triggered when a mouse button is released. | | **MouseMoveEvent** | Event triggered when the mouse is moved. | | **ContextClickEvent** | Event triggered on a context click (right-click). | | **WheelEvent** | Event triggered when the mouse wheel is moved. | | **MouseEnterEvent** | Event triggered when the mouse enters the element. | | **MouseLeaveEvent** | Event triggered when the mouse leaves the element. | | **MouseEnterWindowEvent** | Event triggered when the mouse enters the window containing the element. | | **MouseLeaveWindowEvent** | Event triggered when the mouse leaves the window containing the element. | | **MouseOverEvent** | Event triggered when the mouse is over the element. | | **MouseOutEvent** | Event triggered when the mouse is out of the element. | | **ContextualMenuPopulateEvent** | Event triggered to populate the contextual menu. | | **AttachToPanelEvent** | Event triggered when the element is attached to a panel. | | **DetachFromPanelEvent** | Event triggered when the element is detached from a panel. | | **TooltipEvent** | Event triggered to display a tooltip. | | **IMGUIEvent** | Event triggered for IMGUI rendering. | ## ISubElement This interface serves as an identifier, signifying that within the IRootElement hierarchy, an element exists which can function as a component based on the VisualElement class. ## IRootElement This interface acts as a marker, indicating that an element within the IRootElement hierarchy is capable of functioning as a component derived from the VisualElement class. ## Visual Element Style The imported USS file contains all the classes used by Visual Elements. USS is inspired by CSS and has a similar syntax. To manipulate properties in your code, you should use the techniques described below. ### Changing Element Appearance To change the appearance of a VisualElement, you should manipulate its style properties. For example, to set the top margin of an element to 3: ```csharp element.style.marginTop = 3; ``` ### Reading Element Style Data If you need to read style data from a VisualElement, you should use its resolved style properties. For example, to retrieve the value of the top margin: ```csharp float margin = element.resolvedStyle.marginTop; ``` > You can find the list of all supported properties in the official Unity documentation. ## Visual Element Extensions When working with VisualElements, there are various visual element extensions that allow you to find/clone/replace/etc elements: ```csharp VisualElement rectangle = element.Find("Rectangle"); ``` ```csharp VisualElement elementClone = options.Clone(); ``` ```csharp element.Replace(newElement); ``` # Limitations | Feature | Description | |------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| | **Unique Frame Names** | Each frame must have a unique name to ensure proper functionality. | | **Vector Constraints** | Vectors should be visible, should not contain Image Fills, and, where possible, should be grouped into Unions. | | **Fill Limitations** | Each UI element can contain only a single Fill attribute. | | **Auto-Layout Restrictions** | Stroke borders are not supported within Auto-Layout configurations. | | **Alignment Constraints** | Horizontal and vertical centering cannot be mixed within the same parent element; doing so will default the alignment to center-center. | | **Circle Representation** | For optimal visual rendering, circles should be implemented using rectangles rather than ellipses. | # Not implemented - Documentation (work in progress) - Flex-gap (Unity not supported) - Blur (Unity not supported) - Letter spacing (Unity not supported) - Line height (Unity not supported) - IndividualStrokeWeights (Unity not supported) - ComponentPropertyDefinitions - Various CodeGenerators - Dragging items for scroll-view - Generation of the various states of Elements with states - Generation of the various states of Elements with states- Generation of the various states of Elements with states ================================================ FILE: Readme.md.meta ================================================ fileFormatVersion: 2 guid: 97c5af11bb3cfa4c4952aab609afcd17 TextScriptImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/AssemblyInfo.cs ================================================ #pragma warning disable S2094 using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Figma.Editor")] namespace System.Runtime.CompilerServices { public class IsExternalInit { } } ================================================ FILE: Runtime/AssemblyInfo.cs.meta ================================================ fileFormatVersion: 2 guid: d02acb23f49d34ad996232782b6f0270 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Core/Element.cs ================================================ using UnityEngine; using UnityEngine.UIElements; // ReSharper disable MemberCanBeProtected.Global namespace Figma { using Internals; public abstract class Element : MonoBehaviour, IRootElement { #region Fields string className; #endregion #region Properties public virtual int RootOrder => 0; public string ClassName => className.NotNullOrEmpty() ? className : GetType().Name; public VisualElement Root { get; private set; } public VisualElement[] RootsPreserved { get; private set; } #endregion #region Methods void IRootElement.OnInitialize(VisualElement root, VisualElement[] rootsPreserved) { Root = root; RootsPreserved = rootsPreserved; OnInitialize(); } void IRootElement.OnRebuild() => OnRebuild(); protected virtual void OnInitialize() { } protected virtual void OnRebuild() { } #endregion #region Base Methods protected virtual void Awake() => className = GetType().Name; protected virtual void OnEnable() => className = GetType().Name; protected virtual void OnDisable() { } #endregion } } ================================================ FILE: Runtime/Core/Element.cs.meta ================================================ fileFormatVersion: 2 guid: c3d7de8bb8ca66b41a2a58e3c87acd68 timeCreated: 1524533501 licenseType: Store MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Core/QueryAttribute.cs ================================================ using System; using System.Diagnostics; using UnityEngine.UIElements; // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable MemberCanBeProtected.Global namespace Figma.Attributes { [DebuggerStepThrough] [AttributeUsage(AttributeTargets.Field)] public class QueryAttribute : Attribute { #region Properties public string Path { get; } public string ClassName { get; } [Obsolete("Use 'DownloadImage' instead")] public ElementDownloadImage ImageFiltering { get => DownloadImage; set => DownloadImage = value; } public ElementDownloadImage DownloadImage { get; set; } public bool Hash { get; set; } public string ReplaceElementPath { get; set; } public string RebuildElementEvent { get; set; } public bool StartRoot { get; set; } public bool EndRoot { get; set; } public bool Nullable { get; set; } public bool Hide { get; set; } public bool Localize { get; set; } = true; public string Clicked { get; set; } public string Template { get; set; } public TrickleDown UseTrickleDown { get; set; } public string MouseCaptureOutEvent { get; set; } public string MouseCaptureEvent { get; set; } public string ChangeEvent { get; set; } public string ValidateCommandEvent { get; set; } public string ExecuteCommandEvent { get; set; } public string DragExitedEvent { get; set; } public string DragUpdatedEvent { get; set; } public string DragPerformEvent { get; set; } public string DragEnterEvent { get; set; } public string DragLeaveEvent { get; set; } public string FocusOutEvent { get; set; } public string BlurEvent { get; set; } public string FocusInEvent { get; set; } public string FocusEvent { get; set; } public string InputEvent { get; set; } public string KeyDownEvent { get; set; } public string KeyUpEvent { get; set; } public string GeometryChangedEvent { get; set; } public string PointerDownEvent { get; set; } public string PointerUpEvent { get; set; } public string PointerMoveEvent { get; set; } public string MouseDownEvent { get; set; } public string MouseUpEvent { get; set; } public string MouseMoveEvent { get; set; } public string ContextClickEvent { get; set; } public string WheelEvent { get; set; } public string MouseEnterEvent { get; set; } public string MouseLeaveEvent { get; set; } public string MouseEnterWindowEvent { get; set; } public string MouseLeaveWindowEvent { get; set; } public string MouseOverEvent { get; set; } public string MouseOutEvent { get; set; } public string ContextualMenuPopulateEvent { get; set; } public string AttachToPanelEvent { get; set; } public string DetachFromPanelEvent { get; set; } public string TooltipEvent { get; set; } public string IMGUIEvent { get; set; } #endregion #region Constructors public QueryAttribute(string path, string className = null) { Path = path; ClassName = className; } #endregion } } ================================================ FILE: Runtime/Core/QueryAttribute.cs.meta ================================================ fileFormatVersion: 2 guid: ba43ed1a94fbe41469ea9d6f2b01ca1b timeCreated: 1512009160 licenseType: Store MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Core/UxmlAttribute.cs ================================================ using System; using System.Diagnostics; namespace Figma.Attributes { using static Internals.PathExtensions; [DebuggerStepThrough] [AttributeUsage(AttributeTargets.Class)] public class UxmlAttribute : Attribute { public const string prefix = "Document"; #region Properties public string Root { get; } public string DocumentRoot { get; } public string[] Preserve { get; } public string[] DocumentPreserve { get; } public UxmlDownloadImages DownloadImages { get; } public UxmlElementTypeIdentification TypeIdentification { get; } #endregion #region Constructors public UxmlAttribute(string root = null, UxmlDownloadImages downloadImages = UxmlDownloadImages.Everything, UxmlElementTypeIdentification typeIdentification = UxmlElementTypeIdentification.ByName, params string[] preserve) { Root = root; DocumentRoot = CombinePath(prefix, root); Preserve = preserve; DocumentPreserve = (string[])preserve.Clone(); for (int i = 0; i < preserve.Length; ++i) DocumentPreserve[i] = CombinePath(prefix, DocumentPreserve[i]); DownloadImages = downloadImages; TypeIdentification = typeIdentification; } #endregion } } ================================================ FILE: Runtime/Core/UxmlAttribute.cs.meta ================================================ fileFormatVersion: 2 guid: c1215fc5876343f49f128b37e1181924 timeCreated: 1728307231 ================================================ FILE: Runtime/Core/VisualElementMetadata.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using UnityEngine; using UnityEngine.Pool; using UnityEngine.UIElements; using Debug = UnityEngine.Debug; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedMember.Global namespace Figma { using Attributes; using Internals; using static Internals.PathExtensions; public static class VisualElementMetadata { const int initialCollectionCapacity = 256; record Metadata(UIDocument document, UxmlAttribute uxml, string path); #region Fields static readonly Dictionary rootMetadata = new(initialCollectionCapacity); static readonly List search = new(initialCollectionCapacity); static readonly Dictionary cloneMap = new(initialCollectionCapacity); static readonly List hide = new(initialCollectionCapacity); #endregion #region Properties public static BindingFlags FieldsFlags => BindingFlags.NonPublic | BindingFlags.Instance; public static BindingFlags MethodsFlags => BindingFlags.NonPublic | BindingFlags.Instance; #endregion #region Callbacks public static Action OnInitializeRoot { get; set; } public static Action OnInitializeElement { get; set; } public static Action OnRebuildElement { get; set; } #endregion #region Methods public static void Initialize(UIDocument document, IEnumerable targets) => targets.Where(x => x != null).ForEach(x => Initialize(document, x)); public static void Initialize(UIDocument document, IRootElement target) { Type targetType = target.GetType(); UxmlAttribute uxml = targetType.GetCustomAttribute(); VisualElement targetRoot = document.rootVisualElement.Find(uxml.DocumentRoot, throwException: false, silent: false); if (targetRoot == null) return; rootMetadata.Add(targetRoot, new Metadata(document, uxml, uxml.DocumentRoot)); (string path, VisualElement element)[] rootsPreserved = uxml.DocumentPreserve.Select(x => (x, document.rootVisualElement.Find(x, throwException: false, silent: false))).ToArray(); rootsPreserved.Where(x => !rootMetadata.ContainsKey(x.element)) .ForEach(x => rootMetadata.Add(x.element, new Metadata(document, uxml, x.path))); OnInitializeRoot?.Invoke(targetRoot, document, uxml); Initialize(target, targetType, targetRoot); target.OnInitialize(targetRoot, rootsPreserved.Select(x => x.element).ToArray()); } public static void Initialize(ISubElement target, VisualElement targetRoot) { Initialize(target, target.GetType(), targetRoot); target.OnInitialize(); if (target.GetType().GetCustomAttribute() is { } queryAttribute) OnInitializeElement?.Invoke(targetRoot, null, null, null, null, queryAttribute); } public static void Rebuild(IEnumerable targets) { foreach (IRootElement target in targets.Where(x => x.Root != null)) { target.OnRebuild(); target.Root.Children().ForEach(Rebuild); } } public static void Rebuild(VisualElement target) { if (target is ISubElement targetSubElement) targetSubElement.OnRebuild(); target.Children().ForEach(Rebuild); OnRebuildElement?.Invoke(target); if (hide.Contains(target)) target.Hide(); } public static IEnumerable Search(this VisualElement value, string path, string className = null) where T : VisualElement { static bool StartsWith(string path, VisualElement value, int startIndex) { int endIndex = startIndex + value.name.Length; return path.BeginsWith(value.name, startIndex) && path.Length >= endIndex && (path.Length == endIndex || path[endIndex].IsSeparator()); } static int LastIndexOf(VisualElement root, VisualElement leaf, VisualElement value, string path, int startIndex = 0) { if (value.parent != null && value.parent != root) startIndex = LastIndexOf(root, leaf, value.parent, path, startIndex); if (startIndex < 0 || !StartsWith(path, value, startIndex)) return -1; int endIndex = startIndex + value.name.Length; if (path.Length > endIndex && path[endIndex].IsSeparator() && value != leaf) endIndex++; return endIndex; } static void SearchIn(VisualElement value, string path, int startIndex = 0, string className = null) { static bool EqualsTo(VisualElement value, string path, int startIndex) => path.EqualsTo(value.name, startIndex); search.AddRange(value.Children().Where(child => child.name.NotNullOrEmpty() && EqualsTo(child, path, startIndex) && (className.NullOrEmpty() || child.ClassListContains(className)))); value.Children() .Where(child => child.name.NotNullOrEmpty() && StartsWith(path, child, startIndex)) .ForEach(child => SearchIn(child, path, startIndex + child.name.Length + 1, className)); } static void SearchByFullPath(VisualElement value, string path, int startIndex = 0, string className = null) { static bool EqualsToFullPath(VisualElement root, VisualElement value, string path, int startIndex) => LastIndexOf(root, value, value, path, startIndex) == path.Length; static bool StartsWithFullPath(VisualElement root, VisualElement value, string path, int startIndex) { int endIndex = LastIndexOf(root, value, value, path, startIndex); return endIndex >= 0 && path.Length > endIndex && path[endIndex].IsSeparator(); } search.AddRange(value.Children().Where(child => child.name.NotNullOrEmpty() && EqualsToFullPath(value, child, path, startIndex) && (className.NullOrEmpty() || child.ClassListContains(className)))); value.Children() .Where(child => child.name.NotNullOrEmpty() && StartsWithFullPath(value, child, path, startIndex)) .ForEach(child => SearchByFullPath(child, path, startIndex + child.name.Length + 1, className)); } search.Clear(); VisualElement root = FindRoot(value); if (root != null) { UxmlAttribute uxml = rootMetadata[root].uxml; if (path.BeginsWith(uxml.DocumentRoot) || uxml.DocumentPreserve.Any(x => path.BeginsWith(x))) SearchByFullPath(root.parent.parent.parent, path, 0, className); else SearchIn(value, path, 0, className); } else { SearchByFullPath(value, path, 0, className); } foreach (T result in search.OfType()) yield return result; } public static void Dispose() { VisualElementExtensions.cloneDictionary.Clear(); rootMetadata.Clear(); cloneMap.Clear(); search.Clear(); hide.Clear(); } public static VisualElement FindByPath(this VisualElement root, string path) { foreach (VisualElement child in root.Children()) { VisualElement result = FindByPathRecursive(child, path); if (result != null) return result; } return null; } public static T Find(this VisualElement value, string path, bool throwException = true, bool silent = false) where T : VisualElement { path = path.Replace('\\', unixPathSeperator); T result = value.FindByPath(path).As(); if (result == null && value is TemplateContainer) result = value.Children().First().FindByPath(path).As(); if (result != null) return result; if (throwException) throw new KeyNotFoundException(Extensions.BuildTargetMessage($"Cannot find {typeof(T).Name}", $"{value.GetFullPath()}{Path.DirectorySeparatorChar}{path}")); if (!silent) Debug.LogWarning(Extensions.BuildTargetMessage($"[{nameof(VisualElementMetadata)}] Cannot find {typeof(T).Name}", $"{value.GetFullPath()}{Path.DirectorySeparatorChar}{path}")); return null; } public static (T1, T2) Find(this VisualElement value, string path1, string path2, bool throwException = true, bool silent = false) where T1 : VisualElement where T2 : VisualElement => (value.Find(path1, throwException: throwException, silent: silent), value.Find(path2, throwException: throwException, silent: silent)); public static (T1, T2, T3) Find(this VisualElement value, string path1, string path2, string path3, bool throwException = true, bool silent = false) where T1 : VisualElement where T2 : VisualElement where T3 : VisualElement => (value.Find(path1, throwException: throwException, silent: silent), value.Find(path2, throwException: throwException, silent: silent), value.Find(path3, throwException: throwException, silent: silent)); public static VisualElement Find(this VisualElement value, string path, bool throwException = true, bool silent = true) => Find(value, path, throwException, silent); public static T Clone(this T value, VisualElement parent = null, int index = -1) where T : VisualElement { (TemplateContainer, string) GetNearestTemplate(VisualElement value, string path = "") { while (true) { if (value is TemplateContainer template) return (template, path); if (value.parent is null) return (null, string.Empty); string subPath = CombinePath(value.name, path); value = value.parent; path = subPath; } } parent ??= value.parent; (VisualElement root, string pathToValue) = FindRoot(value, string.Empty); Metadata metadata = rootMetadata[root]; VisualElement temporaryContainer = new(); try { T elementClone; if (cloneMap.ContainsKey(value) && cloneMap[value] is { } template && metadata.document.visualTreeAsset.templateDependencies.FirstOrDefault(x => x.name == template) is { } treeAsset && treeAsset) { treeAsset.CloneTree(temporaryContainer); elementClone = (T)temporaryContainer[0]; } else { (TemplateContainer nearestTemplate, string templatePath) = GetNearestTemplate(value); if (nearestTemplate != null) { string dependencyName = value.GetType().Name; VisualTreeAsset asset = nearestTemplate.templateSource.templateDependencies.FirstOrDefault(x => x.name == dependencyName); if (asset == null && cloneMap.ContainsKey(value) && cloneMap[value] is { } templateNameFallback) asset = nearestTemplate.templateSource.templateDependencies.FirstOrDefault(x => x.name == templateNameFallback); if (asset != null) { elementClone = (T)Activator.CreateInstance(value.GetType()); asset.CloneTree(elementClone.contentContainer); elementClone.CopyStyle(value); value.GetClasses().ForEach(x => elementClone.AddToClassList(x)); } else { nearestTemplate.templateSource.CloneTree(temporaryContainer); elementClone = temporaryContainer.Find(pathToValue, false) ?? temporaryContainer.Find(templatePath); } } else { Debug.LogWarning($"[{nameof(VisualElementMetadata)}] Cloning directly {value.GetType().Name}"); metadata.document.visualTreeAsset.CloneTree(temporaryContainer); elementClone = temporaryContainer.Find(metadata.path).Find(pathToValue); } } elementClone.RemoveFromHierarchy(); parent.Add(elementClone); if (value.parent == elementClone.parent) elementClone.PlaceBehind(value); if (index >= 0) elementClone.name = $"{value.name} {nameof(VisualElement)}:{index}"; parent.MarkDirtyRepaint(); if (elementClone is ISubElement subElement) { Initialize(subElement, elementClone); Rebuild(elementClone); } elementClone.MarginMe(); return elementClone; } catch (Exception exception) { throw new ArgumentException(Extensions.BuildTargetMessage($"Cannot clone {typeof(T).Name}", value.name), exception); } finally { temporaryContainer.RemoveFromHierarchy(); temporaryContainer.Clear(); temporaryContainer.MarkDirtyRepaint(); } } public static VisualElement Clone(this VisualElement value, VisualElement parent = null, int index = -1) => Clone(value, parent, index); public static T Replace(this VisualElement value, VisualElement prefab) where T : VisualElement { VisualElement parent = value.parent; T elementClone = (T)prefab.Clone(parent); if (value.resolvedStyle.position == Position.Relative) { elementClone.style.position = value.style.position; elementClone.style.left = value.style.left; elementClone.style.top = value.style.top; elementClone.style.bottom = value.style.bottom; elementClone.style.right = value.style.right; } else { elementClone.style.alignItems = value.resolvedStyle.alignItems; elementClone.style.alignContent = value.resolvedStyle.alignContent; elementClone.style.justifyContent = value.resolvedStyle.justifyContent; elementClone.style.flexGrow = value.resolvedStyle.flexGrow; elementClone.style.flexShrink = value.resolvedStyle.flexShrink; elementClone.style.flexDirection = value.resolvedStyle.flexDirection; elementClone.style.flexWrap = value.resolvedStyle.flexWrap; elementClone.style.position = value.resolvedStyle.position; elementClone.style.left = value.resolvedStyle.left; elementClone.style.top = value.resolvedStyle.top; elementClone.style.bottom = value.resolvedStyle.bottom; elementClone.style.right = value.resolvedStyle.right; } elementClone.style.alignSelf = value.resolvedStyle.alignSelf; elementClone.name = value.name; elementClone.RemoveFromHierarchy(); parent.Insert(parent.IndexOf(value), elementClone); parent.MarkDirtyRepaint(); value.RemoveFromHierarchy(); value.Clear(); value.MarkDirtyRepaint(); return elementClone; } public static VisualElement Replace(this VisualElement value, VisualElement prefab) => Replace(value, prefab); public static void CopyStyleList(this VisualElement value, VisualElement source) { value.ClearClassList(); source.GetClasses().ForEach(value.AddToClassList); } public static void CopyResolvedStyle(this VisualElement value, VisualElement source, CopyStyleMask copyMask = CopyStyleMask.All) { IStyle style = value.style; IResolvedStyle valueResolvedStyle = value.resolvedStyle; IResolvedStyle sourceResolvedStyle = source.resolvedStyle; if (copyMask.HasFlag(CopyStyleMask.Position)) { style.position = sourceResolvedStyle.position; style.left = sourceResolvedStyle.left; style.right = sourceResolvedStyle.right; style.top = sourceResolvedStyle.top; style.bottom = sourceResolvedStyle.bottom; style.scale = sourceResolvedStyle.scale; style.rotate = sourceResolvedStyle.rotate; } if (copyMask.HasFlag(CopyStyleMask.Size)) { style.width = sourceResolvedStyle.width; style.height = sourceResolvedStyle.height; } if (copyMask.HasFlag(CopyStyleMask.Flex)) { if (valueResolvedStyle.justifyContent != sourceResolvedStyle.justifyContent) style.justifyContent = sourceResolvedStyle.justifyContent; if (valueResolvedStyle.alignSelf != sourceResolvedStyle.alignSelf) style.alignSelf = sourceResolvedStyle.alignSelf; if (valueResolvedStyle.alignItems != sourceResolvedStyle.alignItems) style.alignItems = sourceResolvedStyle.alignItems; if (valueResolvedStyle.alignContent != sourceResolvedStyle.alignContent) style.alignContent = sourceResolvedStyle.alignContent; if (valueResolvedStyle.flexWrap != sourceResolvedStyle.flexWrap) style.flexWrap = sourceResolvedStyle.flexWrap; if (valueResolvedStyle.flexShrink != sourceResolvedStyle.flexShrink) style.flexShrink = sourceResolvedStyle.flexShrink; if (valueResolvedStyle.flexGrow != sourceResolvedStyle.flexGrow) style.flexGrow = sourceResolvedStyle.flexGrow; if (valueResolvedStyle.flexDirection != sourceResolvedStyle.flexDirection) style.flexDirection = sourceResolvedStyle.flexDirection; if (valueResolvedStyle.flexBasis != sourceResolvedStyle.flexBasis) style.flexBasis = sourceResolvedStyle.flexBasis.value; } if (copyMask.HasFlag(CopyStyleMask.Display)) { if (valueResolvedStyle.display != sourceResolvedStyle.display) style.display = sourceResolvedStyle.display; if (valueResolvedStyle.opacity != sourceResolvedStyle.opacity) style.opacity = sourceResolvedStyle.opacity; if (valueResolvedStyle.visibility != sourceResolvedStyle.visibility) style.visibility = sourceResolvedStyle.visibility; if (valueResolvedStyle.unityBackgroundImageTintColor != sourceResolvedStyle.unityBackgroundImageTintColor) style.unityBackgroundImageTintColor = sourceResolvedStyle.unityBackgroundImageTintColor; if (valueResolvedStyle.backgroundPositionX != sourceResolvedStyle.backgroundPositionX) style.backgroundPositionX = sourceResolvedStyle.backgroundPositionX; if (valueResolvedStyle.backgroundPositionY != sourceResolvedStyle.backgroundPositionY) style.backgroundPositionY = sourceResolvedStyle.backgroundPositionY; if (valueResolvedStyle.backgroundRepeat != sourceResolvedStyle.backgroundRepeat) style.backgroundRepeat = sourceResolvedStyle.backgroundRepeat; if (valueResolvedStyle.backgroundSize != sourceResolvedStyle.backgroundSize) style.backgroundSize = sourceResolvedStyle.backgroundSize; if (valueResolvedStyle.backgroundImage != sourceResolvedStyle.backgroundImage) style.backgroundImage = sourceResolvedStyle.backgroundImage; if (valueResolvedStyle.backgroundColor != sourceResolvedStyle.backgroundColor) style.backgroundColor = sourceResolvedStyle.backgroundColor; if (valueResolvedStyle.color != sourceResolvedStyle.color) style.color = sourceResolvedStyle.color; } if (copyMask.HasFlag(CopyStyleMask.Padding)) { if (valueResolvedStyle.paddingTop != sourceResolvedStyle.paddingTop) style.paddingTop = sourceResolvedStyle.paddingTop; if (valueResolvedStyle.paddingRight != sourceResolvedStyle.paddingRight) style.paddingRight = sourceResolvedStyle.paddingRight; if (valueResolvedStyle.paddingLeft != sourceResolvedStyle.paddingLeft) style.paddingLeft = sourceResolvedStyle.paddingLeft; if (valueResolvedStyle.paddingBottom != sourceResolvedStyle.paddingBottom) style.paddingBottom = sourceResolvedStyle.paddingBottom; } if (copyMask.HasFlag(CopyStyleMask.Margins)) { if (valueResolvedStyle.marginTop != sourceResolvedStyle.marginTop) style.marginTop = sourceResolvedStyle.marginTop; if (valueResolvedStyle.marginRight != sourceResolvedStyle.marginRight) style.marginRight = sourceResolvedStyle.marginRight; if (valueResolvedStyle.marginLeft != sourceResolvedStyle.marginLeft) style.marginLeft = sourceResolvedStyle.marginLeft; if (valueResolvedStyle.marginBottom != sourceResolvedStyle.marginBottom) style.marginBottom = sourceResolvedStyle.marginBottom; } if (copyMask.HasFlag(CopyStyleMask.Borders)) { if (valueResolvedStyle.borderTopLeftRadius != sourceResolvedStyle.borderTopLeftRadius) style.borderTopLeftRadius = sourceResolvedStyle.borderTopLeftRadius; if (valueResolvedStyle.borderTopColor != sourceResolvedStyle.borderTopColor) style.borderTopColor = sourceResolvedStyle.borderTopColor; if (valueResolvedStyle.borderRightWidth != sourceResolvedStyle.borderRightWidth) style.borderRightWidth = sourceResolvedStyle.borderRightWidth; if (valueResolvedStyle.borderRightColor != sourceResolvedStyle.borderRightColor) style.borderRightColor = sourceResolvedStyle.borderRightColor; if (valueResolvedStyle.borderLeftWidth != sourceResolvedStyle.borderLeftWidth) style.borderLeftWidth = sourceResolvedStyle.borderLeftWidth; if (valueResolvedStyle.borderLeftColor != sourceResolvedStyle.borderLeftColor) style.borderLeftColor = sourceResolvedStyle.borderLeftColor; if (valueResolvedStyle.borderTopRightRadius != sourceResolvedStyle.borderTopRightRadius) style.borderTopRightRadius = sourceResolvedStyle.borderTopRightRadius; if (valueResolvedStyle.borderBottomWidth != sourceResolvedStyle.borderBottomWidth) style.borderBottomWidth = sourceResolvedStyle.borderBottomWidth; if (valueResolvedStyle.borderBottomLeftRadius != sourceResolvedStyle.borderBottomLeftRadius) style.borderBottomLeftRadius = sourceResolvedStyle.borderBottomLeftRadius; if (valueResolvedStyle.borderBottomColor != sourceResolvedStyle.borderBottomColor) style.borderBottomColor = sourceResolvedStyle.borderBottomColor; if (valueResolvedStyle.borderBottomRightRadius != sourceResolvedStyle.borderBottomRightRadius) style.borderBottomRightRadius = sourceResolvedStyle.borderBottomRightRadius; if (valueResolvedStyle.borderTopWidth != sourceResolvedStyle.borderTopWidth) style.borderTopWidth = sourceResolvedStyle.borderTopWidth; } if (copyMask.HasFlag(CopyStyleMask.Slicing)) { if (valueResolvedStyle.unitySliceTop != sourceResolvedStyle.unitySliceTop) style.unitySliceTop = sourceResolvedStyle.unitySliceTop; if (valueResolvedStyle.unitySliceRight != sourceResolvedStyle.unitySliceRight) style.unitySliceRight = sourceResolvedStyle.unitySliceRight; if (valueResolvedStyle.unitySliceLeft != sourceResolvedStyle.unitySliceLeft) style.unitySliceLeft = sourceResolvedStyle.unitySliceLeft; if (valueResolvedStyle.unitySliceBottom != sourceResolvedStyle.unitySliceBottom) style.unitySliceBottom = sourceResolvedStyle.unitySliceBottom; } if (copyMask.HasFlag(CopyStyleMask.Font)) { if (valueResolvedStyle.whiteSpace != sourceResolvedStyle.whiteSpace) style.whiteSpace = sourceResolvedStyle.whiteSpace; if (valueResolvedStyle.wordSpacing != sourceResolvedStyle.wordSpacing) style.wordSpacing = sourceResolvedStyle.wordSpacing; if (valueResolvedStyle.letterSpacing != sourceResolvedStyle.letterSpacing) style.letterSpacing = sourceResolvedStyle.letterSpacing; if (valueResolvedStyle.textOverflow != sourceResolvedStyle.textOverflow) style.textOverflow = sourceResolvedStyle.textOverflow; if (valueResolvedStyle.fontSize != sourceResolvedStyle.fontSize) style.fontSize = sourceResolvedStyle.fontSize; if (valueResolvedStyle.unityFont != sourceResolvedStyle.unityFont) style.unityFont = new StyleFont { value = sourceResolvedStyle.unityFont }; if (valueResolvedStyle.unityFontDefinition != sourceResolvedStyle.unityFontDefinition) style.unityFontDefinition = sourceResolvedStyle.unityFontDefinition; if (valueResolvedStyle.unityParagraphSpacing != sourceResolvedStyle.unityParagraphSpacing) style.unityParagraphSpacing = sourceResolvedStyle.unityParagraphSpacing; if (valueResolvedStyle.unityTextAlign != sourceResolvedStyle.unityTextAlign) style.unityTextAlign = sourceResolvedStyle.unityTextAlign; if (valueResolvedStyle.unityTextOverflowPosition != sourceResolvedStyle.unityTextOverflowPosition) style.unityTextOverflowPosition = sourceResolvedStyle.unityTextOverflowPosition; if (valueResolvedStyle.unityTextOutlineWidth != sourceResolvedStyle.unityTextOutlineWidth) style.unityTextOutlineWidth = sourceResolvedStyle.unityTextOutlineWidth; if (valueResolvedStyle.unityTextOutlineColor != sourceResolvedStyle.unityTextOutlineColor) style.unityTextOutlineColor = sourceResolvedStyle.unityTextOutlineColor; if (valueResolvedStyle.unityFontStyleAndWeight != sourceResolvedStyle.unityFontStyleAndWeight) style.unityFontStyleAndWeight = sourceResolvedStyle.unityFontStyleAndWeight; } } public static void CopyStyle(this VisualElement value, VisualElement source, CopyStyleMask copyMask = CopyStyleMask.All) { IStyle valueStyle = value.style; IStyle sourceStyle = source.style; if (copyMask.HasFlag(CopyStyleMask.Position)) { if (sourceStyle.position.keyword != StyleKeyword.Null && valueStyle.position != sourceStyle.position) valueStyle.position = sourceStyle.position; if (sourceStyle.top.keyword != StyleKeyword.Null && valueStyle.top != sourceStyle.top) valueStyle.top = sourceStyle.top; if (sourceStyle.bottom.keyword != StyleKeyword.Null && valueStyle.bottom != sourceStyle.bottom) valueStyle.bottom = sourceStyle.bottom; if (sourceStyle.left.keyword != StyleKeyword.Null && valueStyle.left != sourceStyle.left) valueStyle.left = sourceStyle.left; if (sourceStyle.right.keyword != StyleKeyword.Null && valueStyle.right != sourceStyle.right) valueStyle.right = sourceStyle.right; if (sourceStyle.translate.keyword != StyleKeyword.Null && valueStyle.translate != sourceStyle.translate) valueStyle.translate = valueStyle.translate = sourceStyle.translate; if (sourceStyle.rotate.keyword != StyleKeyword.Null && valueStyle.rotate != sourceStyle.rotate) valueStyle.rotate = sourceStyle.rotate; if (sourceStyle.scale.keyword != StyleKeyword.Null && valueStyle.scale != sourceStyle.scale) valueStyle.scale = sourceStyle.scale; if (sourceStyle.transitionTimingFunction.keyword != StyleKeyword.Null && valueStyle.transitionTimingFunction != sourceStyle.transitionTimingFunction) valueStyle.transitionTimingFunction = sourceStyle.transitionTimingFunction; if (sourceStyle.transitionProperty.keyword != StyleKeyword.Null && valueStyle.transitionProperty != sourceStyle.transitionProperty) valueStyle.transitionProperty = sourceStyle.transitionProperty; if (sourceStyle.transitionDuration.keyword != StyleKeyword.Null && valueStyle.transitionDuration != sourceStyle.transitionDuration) valueStyle.transitionDuration = sourceStyle.transitionDuration; if (sourceStyle.transitionDelay.keyword != StyleKeyword.Null && valueStyle.transitionDelay != sourceStyle.transitionDelay) valueStyle.transitionDelay = sourceStyle.transitionDelay; if (sourceStyle.transformOrigin.keyword != StyleKeyword.Null && valueStyle.transformOrigin != sourceStyle.transformOrigin) valueStyle.transformOrigin = sourceStyle.transformOrigin; } if (copyMask.HasFlag(CopyStyleMask.Size)) { if (sourceStyle.width.keyword != StyleKeyword.Null && valueStyle.width != sourceStyle.width) valueStyle.width = sourceStyle.width; if (sourceStyle.minWidth.keyword != StyleKeyword.Null && valueStyle.minWidth != sourceStyle.minWidth) valueStyle.minWidth = sourceStyle.minWidth; if (sourceStyle.maxWidth.keyword != StyleKeyword.Null && valueStyle.maxWidth != sourceStyle.maxWidth) valueStyle.maxWidth = sourceStyle.maxWidth; if (sourceStyle.height.keyword != StyleKeyword.Null && valueStyle.height != sourceStyle.height) valueStyle.height = sourceStyle.height; if (sourceStyle.minHeight.keyword != StyleKeyword.Null && valueStyle.minHeight != sourceStyle.minHeight) valueStyle.minHeight = sourceStyle.minHeight; if (sourceStyle.maxHeight.keyword != StyleKeyword.Null && valueStyle.maxHeight != sourceStyle.maxHeight) valueStyle.maxHeight = sourceStyle.maxHeight; } if (copyMask.HasFlag(CopyStyleMask.Flex)) { if (sourceStyle.alignSelf.keyword != StyleKeyword.Null && valueStyle.alignSelf != sourceStyle.alignSelf) valueStyle.alignSelf = sourceStyle.alignSelf; if (sourceStyle.alignContent.keyword != StyleKeyword.Null && valueStyle.alignContent != sourceStyle.alignContent) valueStyle.alignContent = sourceStyle.alignContent; if (sourceStyle.alignItems.keyword != StyleKeyword.Null && valueStyle.alignItems != sourceStyle.alignItems) valueStyle.alignItems = sourceStyle.alignItems; if (sourceStyle.justifyContent.keyword != StyleKeyword.Null && valueStyle.justifyContent != sourceStyle.justifyContent) valueStyle.justifyContent = sourceStyle.justifyContent; if (sourceStyle.flexDirection.keyword != StyleKeyword.Null && valueStyle.flexDirection != sourceStyle.flexDirection) valueStyle.flexDirection = sourceStyle.flexDirection; if (sourceStyle.flexWrap.keyword != StyleKeyword.Null && valueStyle.flexWrap != sourceStyle.flexWrap) valueStyle.flexWrap = sourceStyle.flexWrap; if (sourceStyle.flexBasis.keyword != StyleKeyword.Null && valueStyle.flexBasis != sourceStyle.flexBasis) valueStyle.flexBasis = sourceStyle.flexBasis.value; if (sourceStyle.flexShrink.keyword != StyleKeyword.Null && valueStyle.flexShrink != sourceStyle.flexShrink) valueStyle.flexShrink = sourceStyle.flexShrink; if (sourceStyle.flexGrow.keyword != StyleKeyword.Null && valueStyle.flexGrow != sourceStyle.flexGrow) valueStyle.flexGrow = sourceStyle.flexGrow; } if (copyMask.HasFlag(CopyStyleMask.Display)) { if (sourceStyle.display.keyword != StyleKeyword.Null && valueStyle.display != sourceStyle.display) valueStyle.display = sourceStyle.display; if (sourceStyle.visibility.keyword != StyleKeyword.Null && valueStyle.visibility != sourceStyle.visibility) valueStyle.visibility = sourceStyle.visibility; if (sourceStyle.opacity.keyword != StyleKeyword.Null && valueStyle.opacity != sourceStyle.opacity) valueStyle.opacity = sourceStyle.opacity; if (sourceStyle.color.keyword != StyleKeyword.Null && valueStyle.color != sourceStyle.color) valueStyle.color = sourceStyle.color; if (sourceStyle.backgroundImage.keyword != StyleKeyword.Null && valueStyle.backgroundImage != sourceStyle.backgroundImage) valueStyle.backgroundImage = sourceStyle.backgroundImage; if (sourceStyle.backgroundColor.keyword != StyleKeyword.Null && valueStyle.backgroundColor != sourceStyle.backgroundColor) valueStyle.backgroundColor = sourceStyle.backgroundColor; if (sourceStyle.unityBackgroundImageTintColor.keyword != StyleKeyword.Null && valueStyle.unityBackgroundImageTintColor != sourceStyle.unityBackgroundImageTintColor) valueStyle.unityBackgroundImageTintColor = sourceStyle.unityBackgroundImageTintColor; if (sourceStyle.backgroundPositionX.keyword != StyleKeyword.Null && valueStyle.backgroundPositionX != sourceStyle.backgroundPositionX) valueStyle.backgroundPositionX = sourceStyle.backgroundPositionX; if (sourceStyle.backgroundPositionY.keyword != StyleKeyword.Null && valueStyle.backgroundPositionY != sourceStyle.backgroundPositionY) valueStyle.backgroundPositionY = sourceStyle.backgroundPositionY; if (sourceStyle.backgroundRepeat.keyword != StyleKeyword.Null && valueStyle.backgroundRepeat != sourceStyle.backgroundRepeat) valueStyle.backgroundRepeat = sourceStyle.backgroundRepeat; if (sourceStyle.backgroundSize.keyword != StyleKeyword.Null && valueStyle.backgroundSize != sourceStyle.backgroundSize) valueStyle.backgroundSize = sourceStyle.backgroundSize; } if (copyMask.HasFlag(CopyStyleMask.Padding)) { if (sourceStyle.paddingTop.keyword != StyleKeyword.Null && valueStyle.paddingTop != sourceStyle.paddingTop) valueStyle.paddingTop = sourceStyle.paddingTop; if (sourceStyle.paddingLeft.keyword != StyleKeyword.Null && valueStyle.paddingLeft != sourceStyle.paddingLeft) valueStyle.paddingLeft = sourceStyle.paddingLeft; if (sourceStyle.paddingRight.keyword != StyleKeyword.Null && valueStyle.paddingRight != sourceStyle.paddingRight) valueStyle.paddingRight = sourceStyle.paddingRight; if (sourceStyle.paddingBottom.keyword != StyleKeyword.Null && valueStyle.paddingBottom != sourceStyle.paddingBottom) valueStyle.paddingBottom = sourceStyle.paddingBottom; } if (copyMask.HasFlag(CopyStyleMask.Margins)) { if (sourceStyle.marginTop.keyword != StyleKeyword.Null && valueStyle.marginTop != sourceStyle.marginTop) valueStyle.marginTop = sourceStyle.marginTop; if (sourceStyle.marginLeft.keyword != StyleKeyword.Null && valueStyle.marginLeft != sourceStyle.marginLeft) valueStyle.marginLeft = sourceStyle.marginLeft; if (sourceStyle.marginRight.keyword != StyleKeyword.Null && valueStyle.marginRight != sourceStyle.marginRight) valueStyle.marginRight = sourceStyle.marginRight; if (sourceStyle.marginBottom.keyword != StyleKeyword.Null && valueStyle.marginBottom != sourceStyle.marginBottom) valueStyle.marginBottom = sourceStyle.marginBottom; } if (copyMask.HasFlag(CopyStyleMask.Borders)) { if (sourceStyle.borderTopColor.keyword != StyleKeyword.Null && valueStyle.borderTopColor != sourceStyle.borderTopColor) valueStyle.borderTopColor = sourceStyle.borderTopColor; if (sourceStyle.borderTopWidth.keyword != StyleKeyword.Null && valueStyle.borderTopWidth != sourceStyle.borderTopWidth) valueStyle.borderTopWidth = sourceStyle.borderTopWidth; if (sourceStyle.borderRightWidth.keyword != StyleKeyword.Null && valueStyle.borderRightWidth != sourceStyle.borderRightWidth) valueStyle.borderRightWidth = sourceStyle.borderRightWidth; if (sourceStyle.borderRightColor.keyword != StyleKeyword.Null && valueStyle.borderRightColor != sourceStyle.borderRightColor) valueStyle.borderRightColor = sourceStyle.borderRightColor; if (sourceStyle.borderLeftWidth.keyword != StyleKeyword.Null && valueStyle.borderLeftWidth != sourceStyle.borderLeftWidth) valueStyle.borderLeftWidth = sourceStyle.borderLeftWidth; if (sourceStyle.borderLeftColor.keyword != StyleKeyword.Null && valueStyle.borderLeftColor != sourceStyle.borderLeftColor) valueStyle.borderLeftColor = sourceStyle.borderLeftColor; if (sourceStyle.borderBottomWidth.keyword != StyleKeyword.Null && valueStyle.borderBottomWidth != sourceStyle.borderBottomWidth) valueStyle.borderBottomWidth = sourceStyle.borderBottomWidth; if (sourceStyle.borderBottomColor.keyword != StyleKeyword.Null && valueStyle.borderBottomColor != sourceStyle.borderBottomColor) valueStyle.borderBottomColor = sourceStyle.borderBottomColor; if (sourceStyle.borderTopLeftRadius.keyword != StyleKeyword.Null && valueStyle.borderTopLeftRadius != sourceStyle.borderTopLeftRadius) valueStyle.borderTopLeftRadius = sourceStyle.borderTopLeftRadius; if (sourceStyle.borderTopRightRadius.keyword != StyleKeyword.Null && valueStyle.borderTopRightRadius != sourceStyle.borderTopRightRadius) valueStyle.borderTopRightRadius = sourceStyle.borderTopRightRadius; if (sourceStyle.borderBottomLeftRadius.keyword != StyleKeyword.Null && valueStyle.borderBottomLeftRadius != sourceStyle.borderBottomLeftRadius) valueStyle.borderBottomLeftRadius = sourceStyle.borderBottomLeftRadius; if (sourceStyle.borderBottomRightRadius.keyword != StyleKeyword.Null && valueStyle.borderBottomRightRadius != sourceStyle.borderBottomRightRadius) valueStyle.borderBottomRightRadius = sourceStyle.borderBottomRightRadius; } if (copyMask.HasFlag(CopyStyleMask.Slicing)) { if (sourceStyle.unitySliceLeft.keyword != StyleKeyword.Null && valueStyle.unitySliceLeft != sourceStyle.unitySliceLeft) valueStyle.unitySliceLeft = sourceStyle.unitySliceLeft; if (sourceStyle.unitySliceTop.keyword != StyleKeyword.Null && valueStyle.unitySliceTop != sourceStyle.unitySliceTop) valueStyle.unitySliceTop = sourceStyle.unitySliceTop; if (sourceStyle.unitySliceRight.keyword != StyleKeyword.Null && valueStyle.unitySliceRight != sourceStyle.unitySliceRight) valueStyle.unitySliceRight = sourceStyle.unitySliceRight; if (sourceStyle.unitySliceBottom.keyword != StyleKeyword.Null && valueStyle.unitySliceBottom != sourceStyle.unitySliceBottom) valueStyle.unitySliceBottom = sourceStyle.unitySliceBottom; } if (copyMask.HasFlag(CopyStyleMask.Font)) { if (sourceStyle.fontSize.keyword != StyleKeyword.Null && valueStyle.fontSize != sourceStyle.fontSize) valueStyle.fontSize = sourceStyle.fontSize; if (sourceStyle.wordSpacing.keyword != StyleKeyword.Null && valueStyle.wordSpacing != sourceStyle.wordSpacing) valueStyle.wordSpacing = sourceStyle.wordSpacing; if (sourceStyle.whiteSpace.keyword != StyleKeyword.Null && valueStyle.whiteSpace != sourceStyle.whiteSpace) valueStyle.whiteSpace = sourceStyle.whiteSpace; if (sourceStyle.letterSpacing.keyword != StyleKeyword.Null && valueStyle.letterSpacing != sourceStyle.letterSpacing) valueStyle.letterSpacing = sourceStyle.letterSpacing; if (sourceStyle.textOverflow.keyword != StyleKeyword.Null && valueStyle.textOverflow != sourceStyle.textOverflow) valueStyle.textOverflow = sourceStyle.textOverflow; if (sourceStyle.unityFont.keyword != StyleKeyword.Null && valueStyle.unityFont != sourceStyle.unityFont) valueStyle.unityFont = sourceStyle.unityFont; if (sourceStyle.unityFontDefinition.keyword != StyleKeyword.Null && valueStyle.unityFontDefinition != sourceStyle.unityFontDefinition) valueStyle.unityFontDefinition = sourceStyle.unityFontDefinition; if (sourceStyle.unityTextOverflowPosition.keyword != StyleKeyword.Null && valueStyle.unityTextOverflowPosition != sourceStyle.unityTextOverflowPosition) valueStyle.unityTextOverflowPosition = sourceStyle.unityTextOverflowPosition; if (sourceStyle.unityTextOutlineWidth.keyword != StyleKeyword.Null && valueStyle.unityTextOutlineWidth != sourceStyle.unityTextOutlineWidth) valueStyle.unityTextOutlineWidth = sourceStyle.unityTextOutlineWidth; if (sourceStyle.unityTextOutlineColor.keyword != StyleKeyword.Null && valueStyle.unityTextOutlineColor != sourceStyle.unityTextOutlineColor) valueStyle.unityTextOutlineColor = sourceStyle.unityTextOutlineColor; if (sourceStyle.unityTextAlign.keyword != StyleKeyword.Null && valueStyle.unityTextAlign != sourceStyle.unityTextAlign) valueStyle.unityTextAlign = sourceStyle.unityTextAlign; if (sourceStyle.unityParagraphSpacing.keyword != StyleKeyword.Null && valueStyle.unityParagraphSpacing != sourceStyle.unityParagraphSpacing) valueStyle.unityParagraphSpacing = sourceStyle.unityParagraphSpacing; if (sourceStyle.unityFontStyleAndWeight.keyword != StyleKeyword.Null && valueStyle.unityFontStyleAndWeight != sourceStyle.unityFontStyleAndWeight) valueStyle.unityFontStyleAndWeight = sourceStyle.unityFontStyleAndWeight; } } public static float GetItemSpacing(this ICustomStyle style) => style.TryGetValue(new CustomStyleProperty("--item-spacing"), out float spacing) ? spacing : float.NaN; public static async void MarginMe(this VisualElement value) { static int GetLines(VisualElement value, VisualElement parent, float spacing, bool horizontalDirection) { float valueSize = horizontalDirection ? value.resolvedStyle.width : value.resolvedStyle.height; float parentSize = horizontalDirection ? parent.resolvedStyle.width : parent.resolvedStyle.height; return valueSize.Invalid() || valueSize == 0 ? parent.childCount : (int)(parentSize / ((2 * valueSize + (spacing.Invalid() ? 0 : spacing)) / 2)); } await Awaiters.EndOfFrame; if (!value.IsShowing() || value.parent == null) return; VisualElement parent = value.parent; float spacing = parent.customStyle.GetItemSpacing(); if (spacing.Invalid()) return; using PooledObject> pooledObject = ListPool.Get(out List children); children.AddRange(parent.Children().Where(x => x.resolvedStyle.display is DisplayStyle.Flex && x.resolvedStyle.position is Position.Relative)); if (children.Count == 0) return; bool horizontalDirection = parent.resolvedStyle.flexDirection == FlexDirection.Row; bool hasWrap = parent.resolvedStyle.flexWrap == Wrap.Wrap; if (hasWrap) { for (float i = 0; i < 1; i += Time.deltaTime) { await Awaiters.NextFrame; if (value.resolvedStyle.width > 0) break; } } int lines = hasWrap ? GetLines(value, parent, spacing, horizontalDirection) : children.Count; int index = children.IndexOf(value); float primaryMargin = lines > 0 && (index - 1) % lines != lines - 1 ? spacing : 0; float counterMargin = index >= lines ? spacing : 0; if (index == children.Count - 1) { value.style.marginRight = 0; value.style.marginBottom = 0; } if (horizontalDirection) { if (index > 0) children[index - 1].style.marginRight = primaryMargin; if (index >= lines) children[index - lines].style.marginBottom = counterMargin; } else { if (index > 0) children[index - 1].style.marginBottom = primaryMargin; if (index >= lines) children[index - lines].style.marginRight = counterMargin; } } public static IEnumerable<(FieldInfo field, string path)> GetAllElementsPaths(Type targetType) { UxmlAttribute attribute = targetType.GetCustomAttribute(); string root = attribute == null ? string.Empty : attribute.Root; QueryAttribute queryRoot = null; foreach (FieldInfo field in targetType.GetFields(FieldsFlags)) { QueryAttribute query = field.GetCustomAttribute(); if (query == null) continue; if (query.StartRoot) queryRoot = query; yield return (field, CombinePath(root, queryRoot?.Path, query.Path)); if (query.EndRoot) queryRoot = null; } } #endregion #region Support Methods static VisualElement FindByPathRecursive(this VisualElement root, string path, string subPath = "") { if (root == null) return null; subPath = !string.IsNullOrEmpty(subPath) ? CombinePath(subPath, root.name) : root.name; if (path == subPath) return root; if (!path.StartsWith(subPath + unixPathSeperator) && path != subPath) return null; foreach (VisualElement child in root.Children()) { VisualElement result = FindByPathRecursive(child, path, subPath); if (result != null) return result; } switch (root) { case TemplateContainer { contentContainer: not null } templateContainer: { foreach (VisualElement child in templateContainer.Children().First().Children()) { VisualElement result = FindByPathRecursive(child, path, subPath); if (result != null) return result; } break; } case ScrollView view: { foreach (VisualElement child in view.contentContainer.Children()) { VisualElement result = FindByPathRecursive(child, path, subPath); if (result != null) return result; } break; } } return null; } static VisualElement FindRoot(VisualElement value) { for (int depth = 0; depth < Const.maximumAllowedDepthLimit; depth++) { value = value.parent; if (value == null) return null; if (rootMetadata.ContainsKey(value)) return value; } throw new InvalidOperationException(Const.maximumDepthLimitReachedExceptionMessage); } static (VisualElement value, string path) FindRoot(VisualElement value, string path) { while (true) { if (rootMetadata.ContainsKey(value)) return (value, path); if (value.parent == null) throw new ArgumentNullException(nameof(value)); string name = value.name.Split($" {nameof(VisualElement)}:", StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? value.name; value = value.parent; path = path.NotNullOrEmpty() ? CombinePath(name, path) : name; } } static void Initialize(object target, Type targetType, VisualElement targetRoot, bool throwException = false, bool silent = false) { void RegisterCallback(VisualElement value, QueryAttribute query) { void Add(VisualElement element, string name, TrickleDown trickleDown) where TEventType : EventBase, new() { if (name.NullOrEmpty()) return; MethodInfo methodInfo = targetType.GetMethod(name, MethodsFlags); if (methodInfo != null) element.RegisterCallback((EventCallback)Delegate.CreateDelegate(typeof(EventCallback), target, methodInfo.Name, true), trickleDown); else Debug.LogWarning($"Method '{name}' for element [{value.GetFullPath()}] not found. Maybe the method is private in the base class or does not exist."); } Add(value, query.MouseCaptureOutEvent, query.UseTrickleDown); Add(value, query.MouseCaptureEvent, query.UseTrickleDown); Add(value, query.ValidateCommandEvent, query.UseTrickleDown); Add(value, query.ExecuteCommandEvent, query.UseTrickleDown); #if UNITY_EDITOR Add(value, query.DragExitedEvent, query.UseTrickleDown); Add(value, query.DragUpdatedEvent, query.UseTrickleDown); Add(value, query.DragPerformEvent, query.UseTrickleDown); Add(value, query.DragEnterEvent, query.UseTrickleDown); Add(value, query.DragLeaveEvent, query.UseTrickleDown); #endif Add(value, query.FocusOutEvent, query.UseTrickleDown); Add(value, query.BlurEvent, query.UseTrickleDown); Add(value, query.FocusInEvent, query.UseTrickleDown); Add(value, query.FocusEvent, query.UseTrickleDown); Add(value, query.InputEvent, query.UseTrickleDown); Add(value, query.KeyDownEvent, query.UseTrickleDown); Add(value, query.KeyUpEvent, query.UseTrickleDown); Add(value, query.GeometryChangedEvent, query.UseTrickleDown); Add(value, query.PointerDownEvent, query.UseTrickleDown); Add(value, query.PointerUpEvent, query.UseTrickleDown); Add(value, query.PointerMoveEvent, query.UseTrickleDown); Add(value, query.MouseDownEvent, query.UseTrickleDown); Add(value, query.MouseUpEvent, query.UseTrickleDown); Add(value, query.MouseMoveEvent, query.UseTrickleDown); Add(value, query.ContextClickEvent, query.UseTrickleDown); Add(value, query.WheelEvent, query.UseTrickleDown); Add(value, query.MouseEnterEvent, query.UseTrickleDown); Add(value, query.MouseLeaveEvent, query.UseTrickleDown); Add(value, query.MouseEnterWindowEvent, query.UseTrickleDown); Add(value, query.MouseLeaveWindowEvent, query.UseTrickleDown); Add(value, query.MouseOverEvent, query.UseTrickleDown); Add(value, query.MouseOutEvent, query.UseTrickleDown); Add(value, query.ContextualMenuPopulateEvent, query.UseTrickleDown); Add(value, query.AttachToPanelEvent, query.UseTrickleDown); Add(value, query.DetachFromPanelEvent, query.UseTrickleDown); Add(value, query.TooltipEvent, query.UseTrickleDown); Add(value, query.IMGUIEvent, query.UseTrickleDown); } void RegisterValueChangedCallback(VisualElement value, QueryAttribute query) { EventCallback GetCallback(string name) where TEventType : EventBase, new() { MethodInfo methodInfo = targetType.GetMethod(name, MethodsFlags); if (methodInfo != null) return (EventCallback)Delegate.CreateDelegate(typeof(EventCallback), target, methodInfo.Name, true); throw new NotSupportedException(); } if (query.ChangeEvent.NotNullOrEmpty() && value is TextField textField) textField.RegisterValueChangedCallback(GetCallback>(query.ChangeEvent)); if (query.ChangeEvent.NotNullOrEmpty() && value is Toggle toggleField) toggleField.RegisterValueChangedCallback(GetCallback>(query.ChangeEvent)); if (query.ChangeEvent.NotNullOrEmpty() && value is SliderInt sliderIntField) sliderIntField.RegisterValueChangedCallback(GetCallback>(query.ChangeEvent)); if (query.ChangeEvent.NotNullOrEmpty() && value is INotifyValueChanged notifyFloatValueChanged) notifyFloatValueChanged.RegisterValueChangedCallback(GetCallback>(query.ChangeEvent)); } void AddClicked(VisualElement value, QueryAttribute query) { if (query.Clicked.NullOrEmpty() || value is not Button button) return; MethodInfo methodInfo = targetType.GetMethod(query.Clicked, BindingFlags.NonPublic | BindingFlags.Instance); if (methodInfo != null) button.clicked += (Action)Delegate.CreateDelegate(typeof(Action), target, methodInfo.Name, true); else Debug.LogWarning($"Method '{query.Clicked}' for element [{value.GetFullPath()}] not found. Maybe the method is private in the base class or does not exist."); } void AddTemplate(VisualElement value, QueryAttribute query) { if (query.Template.NullOrEmpty() && !query.Hash) return; if (query.Hash) { cloneMap.Add(value, value.tooltip); value.tooltip = null; } else { cloneMap.Add(value, query.Template); } } VisualElement InitializeElement(FieldInfo field, QueryAttribute queryRoot, QueryAttribute query) { VisualElement ResolveElement(QueryAttribute queryRoot, QueryAttribute query) { VisualElement queryRootElement = targetRoot; if (queryRoot != null && !queryRoot.Path.NullOrEmpty() && queryRoot.Path != query.Path) queryRootElement = targetRoot.Find(queryRoot.Path, false) ?? targetRoot; VisualElement value = queryRootElement.Find(query.Path, !query.Nullable, query.Nullable); if (query.ReplaceElementPath.NotNullOrEmpty()) { if (value != null) { value = value.Replace(targetRoot.Find(query.ReplaceElementPath)); } else { string name = Path.GetFileName(query.Path); if (name == query.Path) { value = targetRoot.Find(query.ReplaceElementPath, throwException, silent)?.Clone(targetRoot); } else { string path = query.Path.Remove(query.Path.Length - name.Length - 1, name.Length + 1); value = targetRoot.Find(query.ReplaceElementPath, throwException, silent)?.Clone(targetRoot.Find(path)); } if (value != null) value.name = name; } } if (query.RebuildElementEvent.NotNullOrEmpty()) { MethodInfo methodInfo = targetType.GetMethod(query.RebuildElementEvent, MethodsFlags); if (methodInfo != null) value = (VisualElement)methodInfo.Invoke(target, new object[] { value }); } if (value == null) return null; Type valueType = value.GetType(); if (valueType != field.FieldType && valueType.IsAssignableFrom(field.FieldType) && field.FieldType != typeof(VisualElement)) { if (throwException) throw new InvalidOperationException($"Element `{value.name}` of type=[{value.GetType()}] cannot be inserted into `{field.Name}` with type=[{field.FieldType}]"); if (!silent) Debug.LogWarning($"[{nameof(VisualElementMetadata)}] Element `{value.name}` of type=[{value.GetType()}] cannot be inserted into `{field.Name}` with type=[{field.FieldType}]"); return null; } field.SetValue(target, value); return value; } if (query == null) throw new ArgumentNullException(nameof(query)); VisualElement element = ResolveElement(queryRoot, query); if (element == null) return null; RegisterCallback(element, query); RegisterValueChangedCallback(element, query); AddClicked(element, query); AddTemplate(element, query); return element; } QueryAttribute queryRoot = null; foreach (FieldInfo field in targetType.GetFields(FieldsFlags)) { QueryAttribute query = field.GetCustomAttribute(); if (query == null) continue; if (query.StartRoot) queryRoot = query; VisualElement element = InitializeElement(field, queryRoot, query); if (element != null) OnInitializeElement?.Invoke(element, target, targetType, field, queryRoot, query); if (query.EndRoot) queryRoot = null; if (element is ISubElement subElement) { Initialize(subElement, field.FieldType, element, throwException, silent); subElement.OnInitialize(); } if (query.Hide) hide.Add(element); } } #endregion } } ================================================ FILE: Runtime/Core/VisualElementMetadata.cs.meta ================================================ fileFormatVersion: 2 guid: 46d8035533165cc498ba4642614403d3 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Core.meta ================================================ fileFormatVersion: 2 guid: a2887f4413b54672bad1ba5d2c6d7350 timeCreated: 1728307245 ================================================ FILE: Runtime/Extensions/EnumerableExtensions.cs ================================================ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; namespace Figma { internal static class EnumerableExtensions { #region Methods internal static async Task ForEachParallelAsync(this IEnumerable elements, int maxConcurrentRequests, Func func, CancellationToken token) { using SemaphoreSlim semaphore = new(maxConcurrentRequests); Task[] tasks = elements.Select(async x => { await semaphore.WaitAsync(token); await func(x); semaphore.Release(); }).ToArray(); await Task.WhenAll(tasks); } internal static IEnumerable> Chunk(this IEnumerable source, int size) { using IEnumerator enumerator = source.GetEnumerator(); bool hasMoreElements = true; do { List chunk = new(size); while (size > chunk.Count && (hasMoreElements = enumerator.MoveNext())) chunk.Add(enumerator.Current); if (chunk.Count == 0) break; yield return chunk; } while (hasMoreElements); } #endregion } } ================================================ FILE: Runtime/Extensions/EnumerableExtensions.cs.meta ================================================ fileFormatVersion: 2 guid: a0dc8b85c82ec86fa8aa435da7d2c758 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Extensions/Extensions.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; namespace Figma.Internals { [DebuggerStepThrough] internal static class Extensions { #region Methods internal static bool NullOrEmpty(this string value) => string.IsNullOrEmpty(value); internal static bool NotNullOrEmpty(this string value) => !string.IsNullOrEmpty(value); internal static bool Invalid(this float value) => float.IsNaN(value) || float.IsInfinity(value); internal static void ForEach(this IEnumerable enumerable, Action action) { foreach (T element in enumerable) action(element); } #if UNITY_EDITOR internal static string BuildTargetMessage(string message, string target, string end = null) => $"{message} [{target}] {end ?? string.Empty}"; #else internal static string BuildTargetMessage(string message, string target, string end = null) => $"{message} [{target}] {end ?? string.Empty}"; #endif #endregion } } ================================================ FILE: Runtime/Extensions/Extensions.cs.meta ================================================ fileFormatVersion: 2 guid: f333cb303f7c42e589d3a706dade2e0a timeCreated: 1727960585 ================================================ FILE: Runtime/Extensions/PathExtensions.cs ================================================ using System; using System.IO; using System.Linq; using System.Text; namespace Figma.Internals { public static class PathExtensions { #region Const public const char unixPathSeperator = '/'; #endregion #region Methods internal static string[] GetFiles(string path, string searchPattern, SearchOption searchOption) => Directory.GetFiles(path, searchPattern, searchOption).Select(x => x.Replace('\\', unixPathSeperator)).ToArray(); internal static string CombinePath(params string[] paths) { if (paths == null || paths.Length == 0) throw new ArgumentNullException(nameof(paths)); StringBuilder path = new(); for (int i = 0; i < paths.Length; i++) { if (string.IsNullOrEmpty(paths[i])) continue; path.Append(paths[i].Replace('\\', unixPathSeperator)); if (i < paths.Length - 1 && paths[i][paths[i].Length - 1] != unixPathSeperator && !string.IsNullOrEmpty(paths[i + 1])) path.Append(unixPathSeperator); } return path.ToString(); } internal static string GetRelativePath(string from, string to) => CombinePath(Path.GetRelativePath(Path.GetDirectoryName(from), Path.GetDirectoryName(to))?.Replace('\\', unixPathSeperator), Path.GetFileName(to)); internal static string RemoveExtension(string path) => CombinePath(Path.GetDirectoryName(path)?.Replace('\\', unixPathSeperator), Path.GetFileNameWithoutExtension(path)); internal static bool IsSeparator(this char ch) => ch == Path.DirectorySeparatorChar || ch == Path.AltDirectorySeparatorChar; internal static bool EqualsTo(this string path, string value, int startIndex = 0) { if (path.Length - startIndex != value.Length) return false; int i, length = value.Length; for (i = 0; i < length; ++i) { if (path[startIndex + i].IsSeparator() && value[i].IsSeparator() || path[startIndex + i] == value[i]) continue; return false; } return i == length; } internal static bool BeginsWith(this string path, string value, int startIndex = 0) { if (path.Length - startIndex < value.Length) return false; int i, length; for (i = 0, length = value.Length; i < length; ++i) { if (path[startIndex + i].IsSeparator() && value[i].IsSeparator() || path[startIndex + i] == value[i]) continue; return false; } return startIndex + i == path.Length || path[startIndex + i].IsSeparator(); } #endregion } } ================================================ FILE: Runtime/Extensions/PathExtensions.cs.meta ================================================ fileFormatVersion: 2 guid: 7967c428b3ee1604e8b5050a79c3278a MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Extensions/VisualElementExtensions.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using UnityEngine.UIElements; namespace Figma { using Internals; public static class VisualElementExtensions { #region Fields internal static readonly Dictionary<(VisualElement prefab, VisualElement parent), IList> cloneDictionary = new(); #endregion #region Methods public static bool HasVisibility(this VisualElement element) => element.style.visibility == Visibility.Visible; public static void MakeVisible(this VisualElement element) => element.style.visibility = Visibility.Visible; public static void MakeInvisible(this VisualElement element) => element.style.visibility = Visibility.Hidden; public static void SetVisibility(this VisualElement element, bool visible) => element.style.visibility = visible ? Visibility.Visible : Visibility.Hidden; public static bool IsShowing(this VisualElement element) => element.resolvedStyle.display == DisplayStyle.Flex; public static void Show(this VisualElement element) { element.style.display = DisplayStyle.Flex; element.MarginMe(); } public static void Hide(this VisualElement element) => element.style.display = DisplayStyle.None; public static void SetDisplay(this VisualElement element, bool visible) { element.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None; if (visible) element.MarginMe(); } public static void Disable(this VisualElement element) => element.pickingMode = PickingMode.Ignore; public static void Enable(this VisualElement element) => element.pickingMode = PickingMode.Position; public static bool IsEnabled(this VisualElement element) => element.pickingMode == PickingMode.Position; public static IList EnsureList(TVisualElement prefab, VisualElement parent) where TVisualElement : VisualElement { (TVisualElement prefab, VisualElement parent) identifier = (prefab, parent); if (!cloneDictionary.TryGetValue(identifier, out IList elements)) cloneDictionary.Add(identifier, elements = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(prefab.GetType()))); return elements; } public static TVisualElement GetElement(this TVisualElement prefab, int index) where TVisualElement : VisualElement => GetElements(prefab, prefab.parent)[index]; public static List GetElements(this TVisualElement prefab) where TVisualElement : VisualElement => GetElements(prefab, prefab.parent); public static List GetElements(this TVisualElement prefab, VisualElement parent) where TVisualElement : VisualElement { if (EnsureList(prefab, parent) is List list) return list; throw new ArgumentException($"Casting from {typeof(List)} to {cloneDictionary[(prefab, parent)]}"); } public static List GetElements(this VisualElement prefab) => GetElements(prefab); public static List GetElements(this VisualElement prefab, VisualElement parent) => GetElements(prefab, parent); public static void Sync(this TVisualElement prefab, VisualElement parent, IEnumerable data, Action onCreateElement = null) where TVisualElement : VisualElement, ISyncElement { IList elements = EnsureList(prefab, parent); int i = 0; foreach (TData value in data) { TVisualElement element; if (i >= elements.Count) { element = prefab.Clone(parent, i); element.Initialize(i); onCreateElement?.Invoke(element); elements.Add(element); } else { element = (TVisualElement)elements[i]; element.Cleanup(); } if (element.IsVisible(i, value)) element.Show(); else element.Hide(); ++i; } for (int j = i; j < elements.Count; ++j) { elements[j].As().Cleanup(); elements[j].As().Hide(); } } public static void Sync(this TVisualElement prefab, VisualElement parent, TCreationData creationData, IEnumerable data, Action onCreateElement = null) where TVisualElement : VisualElement, ISyncElement { IList elements = EnsureList(prefab, parent); int i = 0; foreach (TData value in data) { TVisualElement element; if (i >= elements.Count) { element = prefab.Clone(parent, i); element.Initialize(i, creationData); onCreateElement?.Invoke(element); elements.Add(element); } else { element = (TVisualElement)elements[i]; element.Cleanup(); } if (element.IsVisible(i, value)) element.Show(); else element.Hide(); ++i; } for (int j = i; j < elements.Count; ++j) { elements[j].As().Cleanup(); elements[j].As().Hide(); } } public static T As(this object value) => (T)value; public static string GetFullPath(this VisualElement element) { string result = string.Empty; for (int depth = 0; depth < Const.maximumAllowedDepthLimit; depth++) { if (element == null) return result; result = string.IsNullOrEmpty(result) ? element.name : element.name + PathExtensions.unixPathSeperator + result; element = element.parent; } throw new InvalidOperationException(Const.maximumDepthLimitReachedExceptionMessage); } #endregion } } ================================================ FILE: Runtime/Extensions/VisualElementExtensions.cs.meta ================================================ fileFormatVersion: 2 guid: 84fecb8572fb586409235bea58b5d37e MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Extensions.meta ================================================ fileFormatVersion: 2 guid: 5919892104fee434aa143c8b3a125a90 folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Figma.asmdef ================================================ { "name": "Figma", "rootNamespace": "Figma", "references": [ "AsyncAwaitUtil" ] } ================================================ FILE: Runtime/Figma.asmdef.meta ================================================ fileFormatVersion: 2 guid: 8b8c60060cf03964dbd41b7237ae33c9 AssemblyDefinitionImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Figma.cs ================================================ using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Figma { using Attributes; using System.Threading.Tasks; [DefaultExecutionOrder(-10)] [RequireComponent(typeof(UIDocument))] public class Figma : MonoBehaviour { #region Fields [SerializeField] string fileKey; [SerializeField] bool filter; [SerializeField] bool reorder; [SerializeField] bool waitFrameBeforeRebuild = true; [SerializeField] string[] fontDirectories; #endregion #region Properties public string FileKey => fileKey; public bool Filter => filter; #endregion #region Base Methods void OnEnable() { if (Application.isBatchMode) return; UIDocument document = GetComponent(); if (document.rootVisualElement is null) return; IRootElement[] elements = GetComponentsInChildren(); VisualElementMetadata.Initialize(document, elements); if (!Application.isPlaying) return; #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed OnEnableNextFrameAsync(); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed return; // Do not make MonoBehaviour.OnEnable async because Unity calls async methods after non-async MonoBehaviour methods async Task OnEnableNextFrameAsync() { // Do not change this to Awaiters, since it is breaking the loading. if (waitFrameBeforeRebuild) await new WaitForEndOfFrame(); VisualElementMetadata.Rebuild(elements); VisualElement root = document.rootVisualElement.Q(UxmlAttribute.prefix); if (root is null || !reorder) return; foreach (IRootElement element in elements.Where(x => x.Root is not null).OrderBy(x => x.RootOrder)) { element.Root.RemoveFromHierarchy(); root.Add(element.Root); } } } void OnDestroy() => VisualElementMetadata.Dispose(); #endregion } } ================================================ FILE: Runtime/Figma.cs.meta ================================================ fileFormatVersion: 2 guid: aaa4e5d0dd7bf25488b120aa854832eb MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Interface/Const.cs ================================================ using System.Globalization; namespace Figma { public static class Const { public const int maximumAllowedDepthLimit = 0x10000; // This is a random big number. public const string maximumDepthLimitReachedExceptionMessage = "Maximum depth limit is exceeded."; public static readonly CultureInfo Culture = CultureInfo.GetCultureInfo("en-US"); public const int initialCollectionCapacity = 128; public const string indentCharacters = " "; } } ================================================ FILE: Runtime/Interface/Const.cs.meta ================================================ fileFormatVersion: 2 guid: b406df7817a64f298f19ebf7facbbdb8 timeCreated: 1747643661 ================================================ FILE: Runtime/Interface/Core/SubElements.cs ================================================ using UnityEngine.UIElements; namespace Figma { public abstract class SubVisualElement : VisualElement, ISubElement { #region Methods protected virtual void OnInitialize() { } protected virtual void OnRebuild() { } void ISubElement.OnInitialize() => OnInitialize(); void ISubElement.OnRebuild() => OnRebuild(); #endregion } public abstract class SubButton : Button, ISubElement { #region Methods protected virtual void OnInitialize() { } protected virtual void OnRebuild() { } void ISubElement.OnInitialize() => OnInitialize(); void ISubElement.OnRebuild() => OnRebuild(); #endregion } public abstract class SubLabel : Label, ISubElement { #region Methods protected virtual void OnInitialize() { } protected virtual void OnRebuild() { } void ISubElement.OnInitialize() => OnInitialize(); void ISubElement.OnRebuild() => OnRebuild(); #endregion } public abstract class SubScrollView : ScrollView, ISubElement { #region Methods protected virtual void OnInitialize() { } protected virtual void OnRebuild() { } void ISubElement.OnInitialize() => OnInitialize(); void ISubElement.OnRebuild() => OnRebuild(); #endregion } } ================================================ FILE: Runtime/Interface/Core/SubElements.cs.meta ================================================ fileFormatVersion: 2 guid: 99f85b01fae873ffdac3884951113a85 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Interface/Core/SyncElements.cs ================================================ using System; using System.Collections.Generic; using UnityEngine.UIElements; namespace Figma { public abstract class SyncVisualElement : SubVisualElement, ISyncElement { #region Methods public virtual void Sync(VisualElement parent, IEnumerable data) => VisualElementExtensions.Sync(this, parent, data); public abstract bool IsVisible(int index, T data); #endregion } public abstract class SyncVisualElement : SubVisualElement, ISyncElement { #region Methods public virtual void Sync(VisualElement parent, TCreationData creationData, IEnumerable data) => VisualElementExtensions.Sync(this, parent, creationData, data); public abstract void Initialize(int index, TCreationData creationData); public abstract bool IsVisible(int index, TData data); #endregion } public abstract class SyncButton : SubButton, ISyncElement { #region Methods public virtual void Sync(VisualElement parent, IEnumerable data) => VisualElementExtensions.Sync(this, parent, data); public abstract bool IsVisible(int index, TData data); #endregion } public abstract class SyncButton : SubButton, ISyncElement { #region Methods public virtual void Sync(VisualElement parent, TCreationData creationData, IEnumerable data) => VisualElementExtensions.Sync(this, parent, creationData, data); public abstract void Initialize(int index, TCreationData creationData); public abstract bool IsVisible(int index, TData data); #endregion } public abstract class SyncButtonSimple : SubButton, ISyncElement, TData> { #region Methods public virtual void Sync(VisualElement parent, Action creationData, IEnumerable data) => VisualElementExtensions.Sync(this, parent, creationData, data); public virtual void Initialize(int index, Action creationData) => clicked += () => creationData(index); public abstract bool IsVisible(int index, TData data); #endregion } public abstract class SyncLabel : SubLabel, ISyncElement { #region Methods public virtual void Sync(VisualElement parent, IEnumerable data) => VisualElementExtensions.Sync(this, parent, data); public abstract bool IsVisible(int index, TData data); #endregion } public abstract class SyncLabel : SubLabel, ISyncElement { #region Methods public virtual void Sync(VisualElement parent, TCreationData creationData, IEnumerable data) => VisualElementExtensions.Sync(this, parent, creationData, data); public abstract void Initialize(int index, TCreationData creationData); public abstract bool IsVisible(int index, TData data); #endregion } public abstract class SyncScrollView : SubScrollView, ISyncElement { #region Methods public virtual void Sync(VisualElement parent, IEnumerable data) => VisualElementExtensions.Sync(this, parent, data); public abstract bool IsVisible(int index, TData data); #endregion } public abstract class SyncScrollView : SubScrollView, ISyncElement { #region Methods public virtual void Sync(VisualElement parent, TCreationData creationData, IEnumerable data) => VisualElementExtensions.Sync(this, parent, creationData, data); public abstract void Initialize(int index, TCreationData creationData); public abstract bool IsVisible(int index, TData data); #endregion } } ================================================ FILE: Runtime/Interface/Core/SyncElements.cs.meta ================================================ fileFormatVersion: 2 guid: fd496d4ec1c37d7af931d627ebf86023 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Interface/Core.meta ================================================ fileFormatVersion: 2 guid: 4f9a8b3c90b78d644a304baf92e4c76d folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Interface/Enums.cs ================================================ using System; namespace Figma { [Flags] public enum UxmlDownloadImages { Everything = 0, Nothing = 1 << 0, ImageFills = 1 << 1, RenderAsPng = 1 << 2, RenderAsSvg = 1 << 3, ByElements = 1 << 4 } public enum UxmlElementTypeIdentification { ByName, ByElementType } public enum ElementDownloadImage { Auto, Download, Ignore } [Flags] public enum CopyStyleMask { None = 0, Text = 1 << 0, Position = 1 << 1, Size = 1 << 2, Flex = 1 << 3, Display = 1 << 4, Padding = 1 << 5, Margins = 1 << 6, Borders = 1 << 7, Slicing = 1 << 8, Font = 1 << 9, All = Text | Position | Size | Flex | Display | Padding | Margins | Borders | Slicing | Font } } ================================================ FILE: Runtime/Interface/Enums.cs.meta ================================================ fileFormatVersion: 2 guid: 801e83821cb9e5f408837586af0e8d8f MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Interface/Interface.Core.cs ================================================ using System.Collections.Generic; using UnityEngine.UIElements; #pragma warning disable S1186 // Functions and closures should not be empty namespace Figma { public interface ISyncElement { #region Methods void Sync(VisualElement parent, IEnumerable data); void Initialize(int index) { } bool IsVisible(int index, TData data); void Cleanup() { } #endregion } public interface ISyncElement { #region Methods void Sync(VisualElement parent, TCreationData creationData, IEnumerable data); void Initialize(int index, TCreationData creationData); bool IsVisible(int index, TData data); void Cleanup() { } #endregion } } ================================================ FILE: Runtime/Interface/Interface.Core.cs.meta ================================================ fileFormatVersion: 2 guid: 0162e75c60e15fd4a9bd4da8ac0c62ed MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Interface/Interfaces.cs ================================================ using UnityEngine.UIElements; namespace Figma { public interface ISubElement { #region Methods void OnInitialize() { } // Method is blank intentionally void OnRebuild() { } // Method is blank intentionally #endregion } public interface IRootElement { #region Properties VisualElement Root { get; } int RootOrder => 0; #endregion #region Methods void OnInitialize(VisualElement root, VisualElement[] rootsPreserved) { } // Method is blank intentionally void OnRebuild() { } // Method is blank intentionally #endregion } } ================================================ FILE: Runtime/Interface/Interfaces.cs.meta ================================================ fileFormatVersion: 2 guid: e01966bb91b67f341809945497f0ff2c MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime/Interface.meta ================================================ fileFormatVersion: 2 guid: 1f0b9b39faba9904dbefcb7c550f313d folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: Runtime.meta ================================================ fileFormatVersion: 2 guid: 70ee33d9a3494344ab4d51378a9f394f folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: package.json ================================================ { "name": "com.trackman.figma", "description": "Figma to Unity uxml/uss converter, utilities for VisualTree management in runtime.", "displayName": "Figma for Unity", "version": "1.3.5", "unity": "2022.3", "dependencies": { "com.unity.vectorgraphics": "2.0.0-preview.25", "com.unity.nuget.newtonsoft-json": "3.2.1", "com.trackman.asyncawaitutil": "1.0.7" }, "author": "TrackMan", "type": "trackman.package", "hideInEditor": false } ================================================ FILE: package.json.meta ================================================ fileFormatVersion: 2 guid: 8054faacce927e54296a8be216631c8e TextScriptImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: ~Samples/Scripts/Figma.Samples.asmdef ================================================ { "name": "Figma.Samples", "rootNamespace": "", "references": [ "Figma" ], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false } ================================================ FILE: ~Samples/Scripts/Figma.Samples.asmdef.meta ================================================ fileFormatVersion: 2 guid: 40458b32386acba81b5009a08b0c7d40 AssemblyDefinitionImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: ~Samples/Scripts/Test.cs ================================================ using System.Linq; using UnityEngine.UIElements; using Random = UnityEngine.Random; namespace Figma.Samples { using Attributes; [Uxml("TestPage/TestFrame", UxmlDownloadImages.Everything, UxmlElementTypeIdentification.ByElementType)] public class Test : Element { const int minCircles = 1; const int maxCircles = 7; #region Fields [Query("Header")] Label header; [Query("CloneButton", Clicked = nameof(Clone))] Button cloneButton; [Query("RemoveButton", Clicked = nameof(Remove))] Button removeButton; [Query("CloneContainer", StartRoot = true)] VisualElement cloneContainer; [Query("CloneCircle", EndRoot = true)] PerfectCircle cloneCircle; [Query("SyncButton", Clicked = nameof(Sync))] Button syncButton; [Query("SyncContainer")] VisualElement syncContainer; [Query("SyncContainer/SyncCircle")] PerfectCircle syncCircle; [Query("FunctionDescription", Hide = true)] Label functionDescription; #endregion #region Methods protected override void OnInitialize() => cloneContainer.style.flexWrap = Wrap.NoWrap; protected override void OnRebuild() => header.text = "Welcome to Figma Test Frame!"; void Clone() { if (cloneContainer.childCount == maxCircles) return; cloneCircle.Clone(cloneContainer); } void Remove() { if (cloneContainer.childCount == minCircles) return; cloneContainer.Remove(cloneContainer.Children().First()); } void Sync() { void RandomColor(int index) => syncContainer.Children().ElementAt(index).style.backgroundColor = Random.ColorHSV(); syncCircle.Sync(syncContainer, RandomColor, Enumerable.Range(0, Random.Range(1, maxCircles + 1))); syncCircle.Hide(); functionDescription.Show(); } #endregion } public class PerfectCircle : SyncButtonSimple { public new class UxmlFactory : UxmlFactory { } #region Methods public override bool IsVisible(int index, int data) => true; #endregion } } ================================================ FILE: ~Samples/Scripts/Test.cs.meta ================================================ fileFormatVersion: 2 guid: 51bc2941ace5c496fbf512e17e9a1939 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: ================================================ FILE: ~Samples/Scripts.meta ================================================ fileFormatVersion: 2 guid: a8bf1e60512441341bdf13c14f151aef folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: ~Samples/Test.unity ================================================ %YAML 1.1 %TAG !u! tag:unity3d.com,2011: --- !u!29 &1 OcclusionCullingSettings: m_ObjectHideFlags: 0 serializedVersion: 2 m_OcclusionBakeSettings: smallestOccluder: 5 smallestHole: 0.25 backfaceThreshold: 100 m_SceneGUID: 00000000000000000000000000000000 m_OcclusionCullingData: {fileID: 0} --- !u!104 &2 RenderSettings: m_ObjectHideFlags: 0 serializedVersion: 9 m_Fog: 0 m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_FogMode: 3 m_FogDensity: 0.01 m_LinearFogStart: 0 m_LinearFogEnd: 300 m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} m_AmbientIntensity: 1 m_AmbientMode: 0 m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} m_HaloStrength: 0.5 m_FlareStrength: 1 m_FlareFadeSpeed: 3 m_HaloTexture: {fileID: 0} m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} m_DefaultReflectionMode: 0 m_DefaultReflectionResolution: 128 m_ReflectionBounces: 1 m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} m_IndirectSpecularColor: {r: 0.37311953, g: 0.38074014, b: 0.3587274, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 serializedVersion: 12 m_GIWorkflowMode: 1 m_GISettings: serializedVersion: 2 m_BounceScale: 1 m_IndirectOutputScale: 1 m_AlbedoBoost: 1 m_EnvironmentLightingMode: 0 m_EnableBakedLightmaps: 1 m_EnableRealtimeLightmaps: 0 m_LightmapEditorSettings: serializedVersion: 12 m_Resolution: 2 m_BakeResolution: 40 m_AtlasSize: 1024 m_AO: 0 m_AOMaxDistance: 1 m_CompAOExponent: 1 m_CompAOExponentDirect: 0 m_ExtractAmbientOcclusion: 0 m_Padding: 2 m_LightmapParameters: {fileID: 0} m_LightmapsBakeMode: 1 m_TextureCompression: 1 m_FinalGather: 0 m_FinalGatherFiltering: 1 m_FinalGatherRayCount: 256 m_ReflectionCompression: 2 m_MixedBakeMode: 2 m_BakeBackend: 1 m_PVRSampling: 1 m_PVRDirectSampleCount: 32 m_PVRSampleCount: 512 m_PVRBounces: 2 m_PVREnvironmentSampleCount: 256 m_PVREnvironmentReferencePointCount: 2048 m_PVRFilteringMode: 1 m_PVRDenoiserTypeDirect: 1 m_PVRDenoiserTypeIndirect: 1 m_PVRDenoiserTypeAO: 1 m_PVRFilterTypeDirect: 0 m_PVRFilterTypeIndirect: 0 m_PVRFilterTypeAO: 0 m_PVREnvironmentMIS: 1 m_PVRCulling: 1 m_PVRFilteringGaussRadiusDirect: 1 m_PVRFilteringGaussRadiusIndirect: 5 m_PVRFilteringGaussRadiusAO: 2 m_PVRFilteringAtrousPositionSigmaDirect: 0.5 m_PVRFilteringAtrousPositionSigmaIndirect: 2 m_PVRFilteringAtrousPositionSigmaAO: 1 m_ExportTrainingData: 0 m_TrainingDataDestination: TrainingData m_LightProbeSampleCountMultiplier: 4 m_LightingDataAsset: {fileID: 0} m_LightingSettings: {fileID: 0} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 m_ObjectHideFlags: 0 m_BuildSettings: serializedVersion: 2 agentTypeID: 0 agentRadius: 0.5 agentHeight: 2 agentSlope: 45 agentClimb: 0.4 ledgeDropHeight: 0 maxJumpAcrossDistance: 0 minRegionArea: 2 manualCellSize: 0 cellSize: 0.16666667 manualTileSize: 0 tileSize: 256 accuratePlacement: 0 maxJobWorkers: 0 preserveTilesOutsideBounds: 0 debug: m_Flags: 0 m_NavMeshData: {fileID: 0} --- !u!1001 &1931369715 PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: m_TransformParent: {fileID: 0} m_Modifications: - target: {fileID: -3369783722120399799, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: title value: 11EfaZFWQFIRh68VYRJMBo objectReference: {fileID: 0} - target: {fileID: -3369783722120399799, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: filter value: 1 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_RootOrder value: 1 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - target: {fileID: 2492470867148571057, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} - target: {fileID: 2916098490094507199, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_Name value: Figma objectReference: {fileID: 0} - target: {fileID: 6641805578009137364, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: sourceAsset value: objectReference: {fileID: 9197481963319205126, guid: 6b8f61c2c55889e47baab4478eacf725, type: 3} - target: {fileID: 6641805578009137364, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} propertyPath: m_PanelSettings value: objectReference: {fileID: 11400000, guid: f486055d8ec653edc96c3f3c38380c8f, type: 2} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} --- !u!1 &2022604971 stripped GameObject: m_CorrespondingSourceObject: {fileID: 2916098490094507199, guid: a1decc1df2e53b24285e24bfd721a22c, type: 3} m_PrefabInstance: {fileID: 1931369715} m_PrefabAsset: {fileID: 0} --- !u!114 &2022604975 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2022604971} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 51bc2941ace5c496fbf512e17e9a1939, type: 3} m_Name: m_EditorClassIdentifier: --- !u!1 &2089345444 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - component: {fileID: 2089345447} - component: {fileID: 2089345446} - component: {fileID: 2089345445} m_Layer: 0 m_Name: Camera m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 --- !u!81 &2089345445 AudioListener: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2089345444} m_Enabled: 1 --- !u!20 &2089345446 Camera: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2089345444} m_Enabled: 1 serializedVersion: 2 m_ClearFlags: 1 m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 y: 0 width: 1 height: 1 near clip plane: 0.3 far clip plane: 1000 field of view: 60 orthographic: 0 orthographic size: 5 m_Depth: 0 m_CullingMask: serializedVersion: 2 m_Bits: 4294967295 m_RenderingPath: -1 m_TargetTexture: {fileID: 0} m_TargetDisplay: 0 m_TargetEye: 3 m_HDR: 1 m_AllowMSAA: 1 m_AllowDynamicResolution: 0 m_ForceIntoRT: 0 m_OcclusionCulling: 1 m_StereoConvergence: 10 m_StereoSeparation: 0.022 --- !u!4 &2089345447 Transform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2089345444} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 170.51552, y: 711.87164, z: 653.8373} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ================================================ FILE: ~Samples/Test.unity.meta ================================================ fileFormatVersion: 2 guid: 37c53d635728f3fe3aa97b1d07aa4a2a DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: ================================================ FILE: ~Samples.meta ================================================ fileFormatVersion: 2 guid: f562b0f5dfae4abab3176961a5070e1f folderAsset: yes DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: