-
Notifications
You must be signed in to change notification settings - Fork 668
DYN-9537: Enhance DynamoUnits with auto schema discovery #16529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7bef05e
cada244
5f0f5d6
3951bc7
b9de267
e6da454
ac7b544
f989217
61335e7
999fbcb
3dd67e0
70ae889
ae5a909
695143a
b6447eb
c43d5a6
4c64c79
10b63fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| using Autodesk.DesignScript.Runtime; | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using Autodesk.DesignScript.Runtime; | ||
| using System.Reflection; | ||
| using System.IO; | ||
| using System.Configuration; | ||
| using System.IO; | ||
| using System.Reflection; | ||
| using System.Runtime.InteropServices; | ||
| using ForgeUnits = Autodesk.ForgeUnits; | ||
|
|
||
| namespace DynamoUnits | ||
|
|
@@ -14,13 +15,13 @@ namespace DynamoUnits | |
| public static class Utilities | ||
| { | ||
| private static ForgeUnits.UnitsEngine unitsEngine; | ||
| private static List<string> candidateDirectories = new List<string>(); | ||
|
|
||
| /// <summary> | ||
| /// Path to the directory used load the schema definitions. | ||
| /// </summary> | ||
| [SupressImportIntoVM] | ||
| public static string SchemaDirectory { get; private set; } = Path.Combine(AssemblyDirectory, "unit"); | ||
|
|
||
| public static string SchemaDirectory { get; private set; } = string.Empty; | ||
|
|
||
| static Utilities() | ||
| { | ||
|
|
@@ -32,51 +33,50 @@ static Utilities() | |
| /// </summary> | ||
| internal static void Initialize() | ||
| { | ||
| var assemblyFilePath = Assembly.GetExecutingAssembly().Location; | ||
| // Build candidate schema directories list | ||
| candidateDirectories.Clear(); | ||
benglin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| var assemblyFilePath = Assembly.GetExecutingAssembly().Location; | ||
| var config = ConfigurationManager.OpenExeConfiguration(assemblyFilePath); | ||
| var key = config.AppSettings.Settings["schemaPath"]; | ||
| string path = null; | ||
| if (key != null) | ||
| { | ||
| path = key.Value; | ||
| } | ||
|
|
||
| if (!string.IsNullOrEmpty(path) && Directory.Exists(path)) | ||
| // Add config path if it's valid | ||
| var configPath = config.AppSettings.Settings["schemaPath"]?.Value; | ||
| if (!string.IsNullOrEmpty(configPath) && Directory.Exists(configPath)) | ||
| { | ||
| SchemaDirectory = path; | ||
| candidateDirectories.Add(configPath); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| unitsEngine = new ForgeUnits.UnitsEngine(); | ||
| ForgeUnits.SchemaUtility.addDefinitionsFromFolder(SchemaDirectory, unitsEngine); | ||
| unitsEngine.resolveSchemas(); | ||
| } | ||
| catch | ||
| // Add ASC schema paths from installed components | ||
| AddAscSchemaPaths(candidateDirectories); | ||
|
|
||
| // Add bundled schema directory as final candidate | ||
| candidateDirectories.Add(BundledSchemaDirectory); | ||
|
|
||
| // Try each candidate directory until we find one that works | ||
| foreach (var directory in candidateDirectories) | ||
| { | ||
| unitsEngine = null; | ||
| //There was an issue initializing the schemas at the specified path. | ||
| // Always update SchemaDirectory to the current attempt for clearer error | ||
| // reporting. If loading succeeds, SchemaDirectory reflects the working | ||
| // path. Otherwise, it shows the last path tried, which will be displayed | ||
| // in any thrown exception. If all paths fail, the exception message will | ||
| // show the default bundled schema directory, which should never fail. | ||
| SchemaDirectory = directory; | ||
|
|
||
| unitsEngine = TryLoadSchemaFromDirectory(directory); | ||
| if (unitsEngine != null) | ||
| { | ||
benglin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| break; // Found the schema directory, so stop trying. | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// only use this method during tests - allows setting a different schema location without | ||
| /// worrying about distributing a test configuration file. | ||
| /// </summary> | ||
| internal static void SetTestEngine(string testSchemaDir) | ||
| { | ||
| try | ||
| { | ||
| unitsEngine = new ForgeUnits.UnitsEngine(); | ||
| ForgeUnits.SchemaUtility.addDefinitionsFromFolder(testSchemaDir, unitsEngine); | ||
| unitsEngine.resolveSchemas(); | ||
| } | ||
| catch | ||
| { | ||
| unitsEngine = null; | ||
| //There was an issue initializing the schemas at the specified path. | ||
| } | ||
| unitsEngine = TryLoadSchemaFromDirectory(testSchemaDir); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -148,20 +148,20 @@ internal static ForgeUnits.UnitsEngine ForgeUnitsEngine | |
| { | ||
| if (unitsEngine == null) | ||
| { | ||
| throw new Exception("There was an issue loading Unit Schemas from the specified path: " | ||
| + SchemaDirectory); | ||
| var attemptedPaths = string.Join(", ", candidateDirectories); | ||
| throw new Exception($"There was an issue loading Unit Schemas. Attempted paths: {attemptedPaths}"); | ||
| } | ||
|
|
||
| return unitsEngine; | ||
| } | ||
| } | ||
|
|
||
| private static string AssemblyDirectory | ||
| private static string BundledSchemaDirectory | ||
| { | ||
| get | ||
| { | ||
| string path = Assembly.GetExecutingAssembly().Location; | ||
| return Path.GetDirectoryName(path); | ||
| return Path.Combine(Path.GetDirectoryName(path), "unit"); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -404,5 +404,129 @@ internal static bool TryParseTypeId(string typeId, out string typeName, out Vers | |
|
|
||
| return false; | ||
| } | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As shown here, |
||
| /// <summary> | ||
| /// Adds ASC (Autodesk Shared Components) schema paths to the candidate directories list. | ||
| /// | ||
| /// We use 'AscSdkWrapper' directly here because 'InstalledAscLookUp' is overkill -- it's got | ||
| /// extra logic we don't need. 'AscSdkWrapper' gives us just the version/path info for ASC | ||
| /// installs, which is all we care about for schema discovery. This also decouples us from | ||
| /// changes in 'InstalledAscLookUp' that could break or complicate schema path resolution. | ||
| /// </summary> | ||
| /// <param name="candidateDirectories">List to add discovered ASC schema paths to</param> | ||
| private static void AddAscSchemaPaths(List<string> candidateDirectories) | ||
| { | ||
| // Currently ASC discovery is only available on Windows. When cross-platform ASC | ||
| // support becomes available, we can extend this to work on other platforms. | ||
| if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| // Use reflection to dynamically load DynamoInstallDetective at runtime. | ||
| // This avoids the need for a direct project reference and InternalsVisibleTo, | ||
| // maintaining cross-platform compatibility for the DynamoUnits library. | ||
| var dynamoInstallDetectiveAssembly = Assembly.LoadFrom( | ||
| Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), | ||
| "DynamoInstallDetective.dll")); | ||
|
|
||
| var ascWrapperType = dynamoInstallDetectiveAssembly.GetType("DynamoInstallDetective.AscSdkWrapper"); | ||
| if (ascWrapperType == null) | ||
| { | ||
| return; // AscSdkWrapper type not found | ||
| } | ||
|
|
||
| // Get major versions using reflection: AscSdkWrapper.GetMajorVersions() | ||
| var getMajorVersionsMethod = ascWrapperType.GetMethod("GetMajorVersions", | ||
| BindingFlags.Public | BindingFlags.Static); | ||
| var majorVersions = (string[])getMajorVersionsMethod?.Invoke(null, null); | ||
|
|
||
| if (majorVersions == null) return; | ||
|
|
||
| // Get the ASC_STATUS enum for comparison | ||
| var ascStatusType = ascWrapperType.GetNestedType("ASC_STATUS"); | ||
| var successValue = Enum.Parse(ascStatusType, "SUCCESS"); | ||
|
|
||
| foreach (var majorVersion in majorVersions) | ||
| { | ||
| // Create AscSdkWrapper instance: new AscSdkWrapper(majorVersion) | ||
| var ascWrapper = Activator.CreateInstance(ascWrapperType, majorVersion); | ||
|
|
||
| // Call GetInstalledPath using reflection | ||
| var getInstalledPathMethod = ascWrapperType.GetMethod("GetInstalledPath"); | ||
| var parameters = new object[] { string.Empty }; | ||
| var result = getInstalledPathMethod?.Invoke(ascWrapper, parameters); | ||
|
|
||
| // Check if result equals ASC_STATUS.SUCCESS | ||
| if (result != null && result.Equals(successValue)) | ||
| { | ||
| var installPath = (string)parameters[0]; | ||
| var schemaPath = Path.Combine(installPath, "coreschemas", "unit"); | ||
| candidateDirectories.Add(schemaPath); | ||
| } | ||
| } | ||
| } | ||
| catch | ||
| { | ||
| // Ignore errors when discovering ASC paths - this is optional discovery. | ||
| // DynamoInstallDetective.dll might not be available on some deployments, | ||
| // or ASC might not be installed on the system. | ||
| } | ||
| } | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| /// <summary> | ||
| /// Attempts to load schema from the specified directory and create a UnitsEngine. | ||
| /// </summary> | ||
| /// <param name="schemaDirectory">Directory containing the schema definitions</param> | ||
| /// <returns>A ForgeUnits.UnitsEngine instance, or null if loading failed</returns> | ||
| private static ForgeUnits.UnitsEngine TryLoadSchemaFromDirectory(string schemaDirectory) | ||
| { | ||
| try | ||
| { | ||
| // Validate that the directory exists and contains required subdirectories | ||
| if (IsValidSchemaDirectory(schemaDirectory)) | ||
| { | ||
| var engine = new ForgeUnits.UnitsEngine(); | ||
| ForgeUnits.SchemaUtility.addDefinitionsFromFolder(schemaDirectory, engine); | ||
| engine.resolveSchemas(); | ||
| return engine; | ||
| } | ||
|
|
||
| return null; // Invalid schema directory | ||
| } | ||
| catch | ||
| { | ||
| //There was an issue initializing the schemas at the specified path. | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Validates that a directory contains the required schema subdirectories. | ||
| /// </summary> | ||
| /// <param name="schemaDirectory">Directory to validate</param> | ||
| /// <returns>True if the directory contains all required subdirectories</returns> | ||
| private static bool IsValidSchemaDirectory(string schemaDirectory) | ||
| { | ||
| if (string.IsNullOrEmpty(schemaDirectory) || !Directory.Exists(schemaDirectory)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| var requiredSubdirectories = new[] { "dimension", "quantity", "symbol", "unit" }; | ||
|
|
||
| foreach (var subdirectory in requiredSubdirectories) | ||
| { | ||
| var subdirectoryPath = Path.Combine(schemaDirectory, subdirectory); | ||
| if (!Directory.Exists(subdirectoryPath)) | ||
| { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| using DynamoUnits; | ||
| using DynamoUnits; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| using System; | ||
| using System.Linq; | ||
| using System.Collections.Generic; | ||
|
|
@@ -65,17 +65,20 @@ private static double CalculateMillimeterPerUnit(Unit dynamoUnit) | |
| { | ||
| if (dynamoUnit != null) | ||
| { | ||
| const string millimeters = "autodesk.unit.unit:millimeters"; | ||
| var mm = Unit.ByTypeID($"{millimeters}-1.0.1"); | ||
| var mm = Unit.ByTypeID($"autodesk.unit.unit:millimeters-1.0.1"); | ||
|
|
||
| if (!dynamoUnit.ConvertibleUnits.Contains(mm)) | ||
| // Match millimeters `Unit` by name, not type ID, to handle different schema versions (users may | ||
| // have "millimeters-1.0.1" or "millimeters-2.0.0" in ConvertibleUnits depending on their setup) | ||
| var convertibleMm = dynamoUnit.ConvertibleUnits.FirstOrDefault(unit => unit.Name == mm.Name); | ||
| if (convertibleMm == null) | ||
| { | ||
| throw new Exception($"{dynamoUnit.Name} was not convertible to mm"); | ||
| } | ||
| return Utilities.ConvertByUnits(1, dynamoUnit, mm); | ||
|
|
||
| return Utilities.ConvertByUnits(1, dynamoUnit, convertibleMm); | ||
| } | ||
|
|
||
| return -1; | ||
| } | ||
|
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -766,6 +766,7 @@ internal class ForgeUnitsTests : UnitTestBase | |
| [Test, Category("UnitTests")] | ||
| public void CanCreateForgeUnitType_FromLoadedTypeString() | ||
| { | ||
| // Exact version exists: use the version as-specified | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test environment now includes |
||
| var unitType = Unit.ByTypeID($"{milimeters}-1.0.1"); | ||
| Assert.NotNull(unitType); | ||
| Assert.AreEqual("Millimeters", unitType.Name); | ||
|
|
@@ -774,18 +775,20 @@ public void CanCreateForgeUnitType_FromLoadedTypeString() | |
| [Test, Category("UnitTests")] | ||
| public void CanCreateForgeUnitType_FromFutureTypeString() | ||
| { | ||
| var unitType = Unit.ByTypeID($"{milimeters}-1.0.2"); | ||
| // Unknown future version: use the latest known version | ||
| var unitType = Unit.ByTypeID($"{milimeters}-99.9.9"); | ||
| Assert.NotNull(unitType); | ||
| Assert.AreEqual("Millimeters", unitType.Name); | ||
| Assert.AreEqual($"{milimeters}-1.0.1", unitType.TypeId); | ||
| Assert.AreEqual($"{milimeters}-2.0.0", unitType.TypeId); | ||
| } | ||
| [Test, Category("UnitTests")] | ||
| public void CanCreateForgeUnitType_FromPastTypeString() | ||
| { | ||
| var unitType = Unit.ByTypeID($"{milimeters}-1.0.0"); | ||
| // Unknown past version: use the latest known version | ||
| var unitType = Unit.ByTypeID($"{milimeters}-0.0.1"); | ||
| Assert.NotNull(unitType); | ||
| Assert.AreEqual("Millimeters", unitType.Name); | ||
| Assert.AreEqual($"{milimeters}-1.0.1", unitType.TypeId); | ||
| Assert.AreEqual($"{milimeters}-2.0.0", unitType.TypeId); | ||
| } | ||
| [Test, Category("UnitTests")] | ||
| public void ForgeUnitEquality() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
Initializemethod now builds a list of candidate schema directories, giving ASC-based directories the highest priority. Detection of ASC directories is handled entirely inAddAscSchemaPaths. If schemas from an ASC directory load successfully, that directory is used and no further lookup is performed.