diff --git a/.gitignore b/.gitignore index 8bd2635..d691a1c 100644 --- a/.gitignore +++ b/.gitignore @@ -355,4 +355,7 @@ MigrationBackup/ # ExceptionLessConfig -exceptionless.txt \ No newline at end of file +exceptionless.txt + +# Local agent workflow state +.superpowers/ diff --git a/Installer/SolidWorksLookup.aip b/Installer/SolidWorksLookup.aip index 2db9f6d..592e620 100644 --- a/Installer/SolidWorksLookup.aip +++ b/Installer/SolidWorksLookup.aip @@ -7,8 +7,8 @@ - - + + @@ -93,7 +93,7 @@ - + @@ -107,13 +107,7 @@ - - - - - - @@ -123,7 +117,6 @@ - @@ -132,10 +125,7 @@ - - - @@ -143,26 +133,14 @@ - - - - - - - - - - - - @@ -394,6 +372,6 @@ - + diff --git a/SldWorksLookup.sln b/SldWorksLookup.sln index 8c90cd7..7daa64a 100644 --- a/SldWorksLookup.sln +++ b/SldWorksLookup.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.29613.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SldWorksLookup", "SldWorksLookup\SldWorksLookup.csproj", "{94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SldWorksLookup.RegressionTests", "tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj", "{4F5C2952-0D72-4F69-9294-9C8E5764086D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,10 +19,18 @@ Global {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Debug|x64.ActiveCfg = Debug|x64 {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Debug|x64.Build.0 = Debug|x64 - {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|Any CPU.Build.0 = Debug|Any CPU - {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.ActiveCfg = Debug|x64 - {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.Build.0 = Debug|x64 + {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|Any CPU.Build.0 = Release|Any CPU + {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.ActiveCfg = Release|x64 + {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.Build.0 = Release|x64 + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|x64.ActiveCfg = Debug|x64 + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|x64.Build.0 = Debug|x64 + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|Any CPU.Build.0 = Release|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|x64.ActiveCfg = Release|x64 + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SldWorksLookup/AddIn.cs b/SldWorksLookup/AddIn.cs index fd103ad..7e0b5a4 100644 --- a/SldWorksLookup/AddIn.cs +++ b/SldWorksLookup/AddIn.cs @@ -15,6 +15,7 @@ using System; using System.Linq; using Xarial.XCad.SolidWorks.Enums; +using SldWorksLookup.Helper; namespace SldWorksLookup { @@ -24,6 +25,7 @@ namespace SldWorksLookup [Icon(typeof(Resource),nameof(Resource.BrowseData_16x))] public class AddIn:SwAddInEx { + private readonly List _captureWindows = new List(); public override void OnConnect() { @@ -135,9 +137,13 @@ private void CmdGroup_CommandClick(Command_e spec) } catch (Exception ex) { - //ex.ToExceptionless(LogExtension.Client) - // .AddTags($"CmdError:{spec}") - // .Submit(); + Application.ShowMessageBox(ExceptionUtil.GetUserMessage(ex), MessageBoxIcon_e.Error); + if (LogExtension.Client != null) + { + ex.ToExceptionless(LogExtension.Client) + .AddTags($"CmdError:{spec}") + .Submit(); + } } } @@ -173,35 +179,48 @@ private void SnoopAdvancedHole() try { var featData = feat.GetDefinition() as IAdvancedHoleFeatureData; - - featData.AccessSelections(doc, null); - - var elems =(featData.GetNearSideElements() as object[]).Cast().ToList(); - - var types = new List() { - typeof(ICounterboreElementData), - typeof(ICountersinkElementData), - typeof(IStraightElementData), - typeof(IStraightTapElementData ), - typeof(ITaperedTapElementData)}; - - foreach (var ele in elems) - { - var matchtype = types.FirstOrDefault(p => p.IsInstanceOfType(ele)); - - ins.Add(InstanceProperty.Create(ele,matchtype ?? typeof(IAdvancedHoleElementData))); - } - ins.Add(InstanceProperty.Create("Base Type", typeof(string))); - foreach (var ele in elems) + if (featData == null) { - ins.Add(InstanceProperty.Create(ele,typeof(IAdvancedHoleElementData))); + Application.ShowMessageBox("Cannot read advancedhole definition"); + return; } - featData.ReleaseSelectionAccess(); + SelectionAccessScope.Run( + () => featData.AccessSelections(doc, null), + () => featData.ReleaseSelectionAccess(), + () => + { + var nearSideElements = featData.GetNearSideElements() as object[]; + if (nearSideElements == null) + throw new InvalidOperationException("Advancedhole near-side elements are unavailable."); + + var elems = nearSideElements + .OfType() + .ToList(); + + var types = new List() { + typeof(ICounterboreElementData), + typeof(ICountersinkElementData), + typeof(IStraightElementData), + typeof(IStraightTapElementData ), + typeof(ITaperedTapElementData)}; + + foreach (var ele in elems) + { + var matchtype = types.FirstOrDefault(p => p.IsInstanceOfType(ele)); + + ins.Add(InstanceProperty.Create(ele, matchtype ?? typeof(IAdvancedHoleElementData))); + } + ins.Add(InstanceProperty.Create("Base Type", typeof(string))); + foreach (var ele in elems) + { + ins.Add(InstanceProperty.Create(ele, typeof(IAdvancedHoleElementData))); + } + }); } catch (System.Exception ex) { - Application.Sw.SendMsgToUser($"{ex.Message},{type} Cannot match a SolidWorks Interface"); + Application.Sw.SendMsgToUser($"{ExceptionUtil.GetUserMessage(ex)},{type} Cannot match a SolidWorks Interface"); } var selPpopWindow = CreatePopupWindow(); @@ -218,6 +237,8 @@ private void ShowColorWindow() private void ShowCaptureWindow() { var window = new CaptureCmd(Application); + _captureWindows.Add(window); + window.Closed += (sender, args) => _captureWindows.Remove(window); window?.Show(); } @@ -227,6 +248,7 @@ private void GetObject() if (doc == null) { Application.ShowMessageBox($"No active doc"); + return; } var getObjectVM = new GetObjectByPIDWindowViewModel(doc, this.Application); var window = CreatePopupWindow(); @@ -324,6 +346,11 @@ private void SnoopPID() public override void OnDisconnect() { + foreach (var window in _captureWindows.ToList()) + { + window.Close(); + } + _captureWindows.Clear(); LogExtension.LogEnded(); } } diff --git a/SldWorksLookup/Helper/ConstructionCleanup.cs b/SldWorksLookup/Helper/ConstructionCleanup.cs new file mode 100644 index 0000000..188444c --- /dev/null +++ b/SldWorksLookup/Helper/ConstructionCleanup.cs @@ -0,0 +1,20 @@ +using System; + +namespace SldWorksLookup.Helper +{ + internal static class ConstructionCleanup + { + public static void Run(Action initialize, Action cleanup) + { + try + { + initialize(); + } + catch + { + cleanup(); + throw; + } + } + } +} diff --git a/SldWorksLookup/Helper/EditScope.cs b/SldWorksLookup/Helper/EditScope.cs new file mode 100644 index 0000000..9812163 --- /dev/null +++ b/SldWorksLookup/Helper/EditScope.cs @@ -0,0 +1,20 @@ +using System; + +namespace SldWorksLookup.Helper +{ + internal static class EditScope + { + public static void Run(Action enter, Action exit, Action body) + { + enter(); + try + { + body(); + } + finally + { + exit(); + } + } + } +} diff --git a/SldWorksLookup/Helper/ExceptionUtil.cs b/SldWorksLookup/Helper/ExceptionUtil.cs new file mode 100644 index 0000000..34b4cb4 --- /dev/null +++ b/SldWorksLookup/Helper/ExceptionUtil.cs @@ -0,0 +1,21 @@ +using System; +using System.Reflection; + +namespace SldWorksLookup.Helper +{ + internal static class ExceptionUtil + { + public static string GetUserMessage(Exception exception) + { + while (exception is TargetInvocationException && exception.InnerException != null) + { + exception = exception.InnerException; + } + + if (!string.IsNullOrWhiteSpace(exception?.Message)) + return exception.Message; + + return "Command failed."; + } + } +} diff --git a/SldWorksLookup/Helper/ObjectMatcherUtil.cs b/SldWorksLookup/Helper/ObjectMatcherUtil.cs index ebada21..f8e0b6e 100644 --- a/SldWorksLookup/Helper/ObjectMatcherUtil.cs +++ b/SldWorksLookup/Helper/ObjectMatcherUtil.cs @@ -15,15 +15,18 @@ public static bool IsArray(this object obj) public static bool IsValueArray(this object obj) { - bool flag = false; if (obj is Array array) { - if (array.Length > 0 && array.GetValue(0).GetType().IsValueType) + foreach (var item in array) { - flag = true; + if (item != null) + { + return item.GetType().IsValueType || item is string; + } } } - return flag; + + return false; } public static IEnumerable ObjToArray(this object obj) diff --git a/SldWorksLookup/Helper/PathExportUtil.cs b/SldWorksLookup/Helper/PathExportUtil.cs index e14a6c0..8d6164e 100644 --- a/SldWorksLookup/Helper/PathExportUtil.cs +++ b/SldWorksLookup/Helper/PathExportUtil.cs @@ -1,11 +1,10 @@ -using Microsoft.VisualBasic; +using Microsoft.VisualBasic; +using SldWorksLookup.PathSplit; using SolidWorks.Interop.sldworks; using SolidWorks.Interop.swconst; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Media.Media3D; namespace SldWorksLookup.Helper @@ -14,118 +13,141 @@ public static class PathExportUtil { public static void Export(SolidWorks.Interop.sldworks.ISldWorks sw) { + if (sw == null) + throw new ArgumentNullException(nameof(sw)); + var modeler = sw.GetModeler() as IModeler; + if (modeler == null) + throw new InvalidOperationException("Cannot get SolidWorks modeler."); var doc = sw.IActiveDoc2; + if (doc == null) + throw new InvalidOperationException("No active document."); + + var selectionManager = doc.ISelectionManager; + if (selectionManager == null) + throw new InvalidOperationException("Cannot get selection manager."); - var feat = doc.ISelectionManager.GetSelectedObject6(1, -1) as IFeature; + var feat = selectionManager.GetSelectedObject6(1, -1) as IFeature; + if (feat == null) + throw new InvalidOperationException("Select a sketch feature before exporting."); var ske = feat.GetSpecificFeature2() as ISketch; + if (ske == null) + throw new InvalidOperationException("Selected feature is not a sketch."); - doc.EditSketch(); + ICurve curve = null; - var ses = (ske.GetSketchSegments() as object[]).Cast().ToList(); + EditScope.Run( + () => doc.EditSketch(), + () => doc.InsertSketch(), + () => + { + var sketchSegmentArray = ske.GetSketchSegments() as object[]; + if (sketchSegmentArray == null || sketchSegmentArray.Length == 0) + throw new InvalidOperationException("Selected sketch has no sketch segments."); - doc.ClearSelection2(true); + var ses = sketchSegmentArray.Cast().ToList(); - for (int i = 0; i < ses.Count; i++) - { - ses[i].Select4(true, null); - } + doc.ClearSelection2(true); - doc.SketchManager.MakeSketchChain(); - doc.ClearSelection2(true); + for (int i = 0; i < ses.Count; i++) + { + ses[i].Select4(true, null); + } - var path = (ske.GetSketchPaths() as object[]).Cast().First(); - - var segs = (path.GetSketchSegments() as object[]).Cast(); + doc.SketchManager.MakeSketchChain(); + doc.ClearSelection2(true); - ICurve curve = null; - foreach (var seg in segs) - { - var seCurve = seg.GetCurve() as ICurve; + var pathArray = ske.GetSketchPaths() as object[]; + if (pathArray == null || pathArray.Length == 0) + throw new InvalidOperationException("No sketch path was generated."); - //剪裁曲线 - GetSpAndEp(seg,out Point3D sp,out Point3D ep); + var path = pathArray.Cast().FirstOrDefault(); + if (path == null) + throw new InvalidOperationException("Generated sketch path is invalid."); - seCurve = seCurve.CreateTrimmedCurve2(sp.X, sp.Y, sp.Z, ep.X, ep.Y, ep.Z); + var pathSegmentArray = path.GetSketchSegments() as object[]; + if (pathSegmentArray == null || pathSegmentArray.Length == 0) + throw new InvalidOperationException("Generated sketch path has no segments."); - var body = seCurve.CreateWireBody(); - body.Display2(doc as PartDoc, Information.RGB(255, 0, 0), (int)swTempBodySelectOptions_e.swTempBodySelectOptionNone); + var segs = pathSegmentArray.Cast(); - if (curve == null) - { - curve = seCurve; - } - else - { - curve = modeler.MergeCurves(new object[] { curve, seCurve }); - } - } + foreach (var seg in segs) + { + var seCurve = seg.GetCurve() as ICurve; + if (seCurve == null) + throw new InvalidOperationException("Cannot get sketch segment curve."); - doc.InsertSketch(); + var wrapper = new SketchSegmentWrapper(seg); + var sp = wrapper.SourceStartPoint; + var ep = wrapper.SourceEndPoint; - var points = SplitCurve(curve,10); + seCurve = seCurve.CreateTrimmedCurve2(sp.X, sp.Y, sp.Z, ep.X, ep.Y, ep.Z); + seCurve = RequireExportValue(seCurve, "Cannot trim sketch segment curve."); - doc.Insert3DSketch(); - var ske3D = doc.SketchManager.ActiveSketch; + var body = seCurve.CreateWireBody(); + body = RequireExportValue(body, "Cannot create wire body for trimmed sketch segment curve."); - foreach (var point in points) - { - doc.SketchManager.CreatePoint(point.X,point.Y,point.Z); + var partDoc = RequireExportValue(doc as PartDoc, "Active document is not a part document."); + body.Display2(partDoc, Information.RGB(255, 0, 0), (int)swTempBodySelectOptions_e.swTempBodySelectOptionNone); - } - } + curve = MergeCurveOrThrow( + curve, + seCurve, + (left, right) => modeler.MergeCurves(new object[] { left, right }) as ICurve, + "while merging sketch path segment"); + } - private static void GetSpAndEp(ISketchSegment seg, out Point3D sp, out Point3D ep) - { - sp = default;ep = default; - switch ((swSketchSegments_e)seg.GetType()) - { - case swSketchSegments_e.swSketchLINE: - var line = seg as ISketchLine; - sp = (line.GetStartPoint2() as ISketchPoint).ToPoint(); - ep = (line.GetEndPoint2() as ISketchPoint).ToPoint(); - break; - case swSketchSegments_e.swSketchARC: - var arc = seg as ISketchArc; - sp = (arc.GetStartPoint2() as ISketchPoint).ToPoint(); - ep = (arc.GetStartPoint2() as ISketchPoint).ToPoint(); - break; - case swSketchSegments_e.swSketchELLIPSE: - var eli= seg as ISketchEllipse; - sp = (eli.GetStartPoint2() as ISketchPoint).ToPoint(); - ep = (eli.GetStartPoint2() as ISketchPoint).ToPoint(); - break; - case swSketchSegments_e.swSketchSPLINE: - var spline = seg as SketchSpline; - var points = (spline.GetPoints2() as object[]).Cast().ToList(); - sp = new Point3D(points[0].X, points[0].Y, points[0].Z); - ep = new Point3D(points[points.Count-1].X, points[points.Count - 1].Y, points[points.Count - 1].Z); - break; - case swSketchSegments_e.swSketchTEXT: - throw new NotSupportedException(); - case swSketchSegments_e.swSketchPARABOLA: - var para = seg as SketchParabola; - sp = (para.GetStartPoint2() as ISketchPoint).ToPoint(); - ep = (para.GetStartPoint2() as ISketchPoint).ToPoint(); - break; - default: - throw new NotSupportedException(); - } + if (curve == null) + throw new InvalidOperationException("Cannot create a merged curve from the generated sketch path."); + }); + + var points = SplitCurve(curve, 10); + + EditScope.Run( + () => doc.Insert3DSketch(), + () => doc.Insert3DSketch(), + () => + { + foreach (var point in points) + { + doc.SketchManager.CreatePoint(point.X, point.Y, point.Z); + } + }); } - public static Point3D ToPoint(this double[] point) + internal static TCurve MergeCurveOrThrow( + TCurve currentCurve, + TCurve nextCurve, + Func mergeCurves, + string context) + where TCurve : class { - return new Point3D(point[0], point[1], point[2]); + if (nextCurve == null) + throw new InvalidOperationException("Cannot merge a null sketch path segment curve. " + context); + if (currentCurve == null) + return nextCurve; + if (mergeCurves == null) + throw new ArgumentNullException(nameof(mergeCurves)); + + var merged = mergeCurves(currentCurve, nextCurve); + if (merged == null) + throw new InvalidOperationException("Cannot merge sketch path curves. " + context); + + return merged; } - public static Point3D ToPoint(this ISketchPoint skePoint) + private static TValue RequireExportValue(TValue value, string message) + where TValue : class { - return new Point3D(skePoint.X, skePoint.Y, skePoint.Z); + if (value == null) + throw new InvalidOperationException(message); + + return value; } - public static List SplitCurve(ICurve curve,int num) + public static List SplitCurve(ICurve curve, int num) { var points = new List(); @@ -142,5 +164,4 @@ public static List SplitCurve(ICurve curve,int num) return points; } } - } diff --git a/SldWorksLookup/Helper/SelectionAccessScope.cs b/SldWorksLookup/Helper/SelectionAccessScope.cs new file mode 100644 index 0000000..86227d1 --- /dev/null +++ b/SldWorksLookup/Helper/SelectionAccessScope.cs @@ -0,0 +1,22 @@ +using System; + +namespace SldWorksLookup.Helper +{ + internal static class SelectionAccessScope + { + public static void Run(Func acquire, Action release, Action body) + { + if (!acquire()) + throw new InvalidOperationException("Cannot access the feature selections."); + + try + { + body(); + } + finally + { + release(); + } + } + } +} diff --git a/SldWorksLookup/Helper/TypeMatcherUtil.cs b/SldWorksLookup/Helper/TypeMatcherUtil.cs index 0bdc718..4b5f1ff 100644 --- a/SldWorksLookup/Helper/TypeMatcherUtil.cs +++ b/SldWorksLookup/Helper/TypeMatcherUtil.cs @@ -17,6 +17,12 @@ public static Type Match(Type sourceType) name = name.Remove(name.Length - 5, 5); } + var interfaceType = sourceType.Assembly.GetType($"{sourceType.Namespace}.I{name}"); + if (interfaceType != null && interfaceType.IsInterface) + { + return interfaceType; + } + var hasValue = SolidWorksTypes.FirstOrDefault(p => p.Item1 == name); return hasValue != null ? hasValue.Item2: sourceType; } @@ -42,7 +48,7 @@ public static Type Match(Type sourceType) new Tuple("BendTable", typeof(IBendTable)), new Tuple("BendTableAnnotation", typeof(IBendTableAnnotation)), new Tuple("BlockDefinition", typeof(IBlockDefinition)), - new Tuple("Blocknstance", typeof(IBlockInstance)), + new Tuple("BlockInstance", typeof(IBlockInstance)), new Tuple("Body", typeof(IBody)), new Tuple("Body2", typeof(IBody2)), new Tuple("BodyFolder", typeof(IBodyFolder)), @@ -205,14 +211,14 @@ public static Type Match(Type sourceType) new Tuple("HoleStandardsData", typeof(IHoleStandardsData)), new Tuple("HoleTable", typeof(IHoleTable)), new Tuple("HoleTableAnnotation", typeof(IHoleTableAnnotation)), - new Tuple("mportDxfDwgData", typeof(IImportDxfDwgData)), - new Tuple("mportedCurveFeatureData", typeof(IImportedCurveFeatureData)), - new Tuple("mportgesData", typeof(IImportIgesData)), - new Tuple("mportStepData", typeof(IImportStepData)), - new Tuple("ndentFeatureData", typeof(IIndentFeatureData)), - new Tuple("nterference", typeof(IInterference)), - new Tuple("nterferenceDetectionMgr", typeof(IInterferenceDetectionMgr)), - new Tuple("ntersectFeatureData", typeof(IIntersectFeatureData)), + new Tuple("ImportDxfDwgData", typeof(IImportDxfDwgData)), + new Tuple("ImportedCurveFeatureData", typeof(IImportedCurveFeatureData)), + new Tuple("ImportIgesData", typeof(IImportIgesData)), + new Tuple("ImportStepData", typeof(IImportStepData)), + new Tuple("IndentFeatureData", typeof(IIndentFeatureData)), + new Tuple("Interference", typeof(IInterference)), + new Tuple("InterferenceDetectionMgr", typeof(IInterferenceDetectionMgr)), + new Tuple("IntersectFeatureData", typeof(IIntersectFeatureData)), new Tuple("JogFeatureData", typeof(IJogFeatureData)), new Tuple("JoinFeatureData", typeof(IJoinFeatureData)), new Tuple("JournalManager", typeof(IJournalManager)), @@ -241,7 +247,7 @@ public static Type Match(Type sourceType) new Tuple("MateEntity", typeof(IMateEntity)), new Tuple("MateEntity2", typeof(IMateEntity2)), new Tuple("MateFeatureData", typeof(IMateFeatureData)), - new Tuple("MatenPlace", typeof(IMateInPlace)), + new Tuple("MateInPlace", typeof(IMateInPlace)), new Tuple("MateLoadReference", typeof(IMateLoadReference)), new Tuple("MateReference", typeof(IMateReference)), new Tuple("MaterialVisualPropertiesData", typeof(IMaterialVisualPropertiesData)), @@ -271,24 +277,8 @@ public static Type Match(Type sourceType) new Tuple("Mouse", typeof(IMouse)), new Tuple("MoveCopyBodyFeatureData", typeof(IMoveCopyBodyFeatureData)), new Tuple("MoveFaceFeatureData", typeof(IMoveFaceFeatureData)), - new Tuple("mportDxfDwgData", typeof(ImportDxfDwgData)), - new Tuple("mportDxfDwgDataClass", typeof(ImportDxfDwgDataClass)), - new Tuple("mportedCurveFeatureData", typeof(ImportedCurveFeatureData)), - new Tuple("mportedCurveFeatureDataClass", typeof(ImportedCurveFeatureDataClass)), - new Tuple("mportgesData", typeof(ImportIgesData)), - new Tuple("mportgesDataClass", typeof(ImportIgesDataClass)), - new Tuple("mportStepData", typeof(ImportStepData)), - new Tuple("mportStepDataClass", typeof(ImportStepDataClass)), new Tuple("MultiJogLeader", typeof(IMultiJogLeader)), - new Tuple("ndentFeatureData", typeof(IndentFeatureData)), - new Tuple("ndentFeatureDataClass", typeof(IndentFeatureDataClass)), new Tuple("Note", typeof(INote)), - new Tuple("nterference", typeof(Interference)), - new Tuple("nterferenceClass", typeof(InterferenceClass)), - new Tuple("nterferenceDetectionMgr", typeof(InterferenceDetectionMgr)), - new Tuple("nterferenceDetectionMgrClass", typeof(InterferenceDetectionMgrClass)), - new Tuple("ntersectFeatureData", typeof(IntersectFeatureData)), - new Tuple("ntersectFeatureDataClass", typeof(IntersectFeatureDataClass)), new Tuple("OneBendFeatureData", typeof(IOneBendFeatureData)), new Tuple("PackAndGo", typeof(IPackAndGo)), new Tuple("PageSetup", typeof(IPageSetup)), @@ -300,15 +290,15 @@ public static Type Match(Type sourceType) new Tuple("PartingSurfaceFeatureData", typeof(IPartingSurfaceFeatureData)), new Tuple("PerpendicularMateFeatureData", typeof(IPerpendicularMateFeatureData)), new Tuple("PlaneManipulator", typeof(IPlaneManipulator)), - new Tuple("PMDatumData", typeof(IPMIDatumData)), - new Tuple("PMDatumFeature", typeof(IPMIDatumFeature)), - new Tuple("PMDatumTarget", typeof(IPMIDatumTarget)), - new Tuple("PMDimensionData", typeof(IPMIDimensionData)), - new Tuple("PMDimensiontem", typeof(IPMIDimensionItem)), - new Tuple("PMFrameData", typeof(IPMIFrameData)), - new Tuple("PMGtolBoxData", typeof(IPMIGtolBoxData)), - new Tuple("PMGtolData", typeof(IPMIGtolData)), - new Tuple("PMGtolFrameDatum", typeof(IPMIGtolFrameDatum)), + new Tuple("PMIDatumData", typeof(IPMIDatumData)), + new Tuple("PMIDatumFeature", typeof(IPMIDatumFeature)), + new Tuple("PMIDatumTarget", typeof(IPMIDatumTarget)), + new Tuple("PMIDimensionData", typeof(IPMIDimensionData)), + new Tuple("PMIDimensionItem", typeof(IPMIDimensionItem)), + new Tuple("PMIFrameData", typeof(IPMIFrameData)), + new Tuple("PMIGtolBoxData", typeof(IPMIGtolBoxData)), + new Tuple("PMIGtolData", typeof(IPMIGtolData)), + new Tuple("PMIGtolFrameDatum", typeof(IPMIGtolFrameDatum)), new Tuple("Print3DDialog", typeof(IPrint3DDialog)), new Tuple("PrintSpecification", typeof(IPrintSpecification)), new Tuple("ProfileCenterMateFeatureData", typeof(IProfileCenterMateFeatureData)), @@ -370,7 +360,7 @@ public static Type Match(Type sourceType) new Tuple("SelectionMgr", typeof(ISelectionMgr)), new Tuple("SelectionSet", typeof(ISelectionSet)), new Tuple("SelectionSetFolder", typeof(ISelectionSetFolder)), - new Tuple("SelectionSettem", typeof(ISelectionSetItem)), + new Tuple("SelectionSetItem", typeof(ISelectionSetItem)), new Tuple("Sensor", typeof(ISensor)), new Tuple("SFSymbol", typeof(ISFSymbol)), new Tuple("Sheet", typeof(ISheet)), @@ -394,7 +384,7 @@ public static Type Match(Type sourceType) new Tuple("Sketch", typeof(ISketch)), new Tuple("SketchArc", typeof(ISketchArc)), new Tuple("SketchBlockDefinition", typeof(ISketchBlockDefinition)), - new Tuple("SketchBlocknstance", typeof(ISketchBlockInstance)), + new Tuple("SketchBlockInstance", typeof(ISketchBlockInstance)), new Tuple("SketchContour", typeof(ISketchContour)), new Tuple("SketchedBendFeatureData", typeof(ISketchedBendFeatureData)), new Tuple("SketchEllipse", typeof(ISketchEllipse)), @@ -468,7 +458,7 @@ public static Type Match(Type sourceType) new Tuple("TitleBlockTableAnnotation", typeof(ITitleBlockTableAnnotation)), new Tuple("TitleBlockTableFeature", typeof(ITitleBlockTableFeature)), new Tuple("ToolingSplitFeatureData", typeof(IToolingSplitFeatureData)), - new Tuple("TreeControltem", typeof(ITreeControlItem)), + new Tuple("TreeControlItem", typeof(ITreeControlItem)), new Tuple("TriadManipulator", typeof(ITriadManipulator)), new Tuple("UniversalJointMateFeatureData", typeof(IUniversalJointMateFeatureData)), new Tuple("UserProgressBar", typeof(IUserProgressBar)), @@ -492,5 +482,3 @@ public static Type Match(Type sourceType) }; } } - - diff --git a/SldWorksLookup/Helper/TypeMatcherUtil.tt b/SldWorksLookup/Helper/TypeMatcherUtil.tt index 0b59ffc..c7b8a83 100644 --- a/SldWorksLookup/Helper/TypeMatcherUtil.tt +++ b/SldWorksLookup/Helper/TypeMatcherUtil.tt @@ -21,6 +21,19 @@ namespace SldWorksLookup public static Type Match(Type sourceType) { var name = sourceType.Name.Split('.').Last(); + + //处理包含 [Type]Class的情况 例如 MathTransformClass + if (name.EndsWith("Class")) + { + name = name.Remove(name.Length - 5, 5); + } + + var interfaceType = sourceType.Assembly.GetType($"{sourceType.Namespace}.I{name}"); + if (interfaceType != null && interfaceType.IsInterface) + { + return interfaceType; + } + var hasValue = SolidWorksTypes.FirstOrDefault(p => p.Item1 == name); return hasValue != null ? hasValue.Item2: sourceType; } @@ -77,14 +90,19 @@ public class TypeMatcher:IComparable foreach (Type type in types) { - if (Regex.IsMatch(type.FullName,"SolidWorks.Interop.sldworks.I[A-Za-z]+")) + if (type.IsInterface && + Regex.IsMatch(type.FullName,"SolidWorks.Interop.sldworks.I[A-Za-z]+") && + type.Name.Length > 1 && + char.IsUpper(type.Name[1])) { var name = type.FullName.Split('.').Last(); - list.Add(new TypeMatcher(name.Replace("I",""), $"typeof({name})")); + list.Add(new TypeMatcher(name.Substring(1), $"typeof({name})")); } } - list.Distinct(); - return list; + return list + .GroupBy(p => p.TypeName) + .Select(p => p.First()) + .ToList(); } -#> \ No newline at end of file +#> diff --git a/SldWorksLookup/Install.bat b/SldWorksLookup/Install.bat index 52ddd48..5a9cb42 100644 --- a/SldWorksLookup/Install.bat +++ b/SldWorksLookup/Install.bat @@ -1,5 +1,8 @@ -set path=%~d0 -cd %path% -cd /d %~dp0 - -RegAsm.exe SldWorksLookup.dll /codebase +@echo off +setlocal +cd /d "%~dp0" || exit /b 1 +if not exist "%~dp0RegAsm.exe" exit /b 2 +if not exist "%~dp0SldWorksLookup.dll" exit /b 3 +"%~dp0RegAsm.exe" "%~dp0SldWorksLookup.dll" /codebase +if errorlevel 1 exit /b %errorlevel% +exit /b 0 diff --git a/SldWorksLookup/LogExtension.cs b/SldWorksLookup/LogExtension.cs index e8c8c72..1d4c473 100644 --- a/SldWorksLookup/LogExtension.cs +++ b/SldWorksLookup/LogExtension.cs @@ -1,57 +1,47 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless; -using System.Text; -using System.Threading.Tasks; +using System; +using System.Diagnostics; using System.IO; -using Xarial.XCad.SolidWorks.Enums; +using Exceptionless; using Exceptionless.Logging; +using Xarial.XCad.SolidWorks.Enums; namespace SldWorksLookup { internal static class LogExtension { public static readonly string LogFolder = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),"SldWorksLookup", + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SldWorksLookup", "Log"); public static ExceptionlessClient Client { get; private set; } internal static void LogStart( - Version version, - SwVersion_e sldWorksVersion, + Version version, + SwVersion_e sldWorksVersion, string userName) { try { var configFile = Path.Combine( - Path.GetDirectoryName(typeof(LogExtension).Assembly.Location), - "exceptionless.txt"); - - string[] data = new string[] { "", "" }; - if (!File.Exists(configFile)) - { - data = File.ReadAllLines(configFile); - } + Path.GetDirectoryName(typeof(LogExtension).Assembly.Location), + "exceptionless.txt"); - - if (data.Length < 2) + string serverUrl; + string apiKey; + if (!TryReadConfiguration(configFile, out serverUrl, out apiKey)) return; Client = new ExceptionlessClient(c => { - c.ServerUrl = data[0].Trim(); - c.ApiKey = data[1].Trim(); + c.ServerUrl = serverUrl; + c.ApiKey = apiKey; c.SetVersion(version); }); - //服务信息收集配置 Client.Configuration.IncludePrivateInformation = true; Client.Configuration.IncludeMachineName = true; Client.Configuration.IncludeIpAddress = true; - - //设置本地存储日志文件夹 + try { if (!Directory.Exists(LogFolder)) @@ -65,7 +55,6 @@ internal static void LogStart( .Submit(); } - //开启心跳追踪 var uid = $"{Environment.UserName}@{Environment.MachineName}"; Client.Configuration.SetUserIdentity(uid, userName ?? uid); Client.Configuration.UseSessions(); @@ -77,7 +66,42 @@ internal static void LogStart( } catch (Exception ex) { + Debug.WriteLine(ex); + } + } + + internal static bool TryReadConfiguration(string path, out string serverUrl, out string apiKey) + { + serverUrl = null; + apiKey = null; + + try + { + if (!File.Exists(path)) + return false; + + var data = File.ReadAllLines(path); + if (data.Length < 2) + return false; + + serverUrl = data[0].Trim(); + apiKey = data[1].Trim(); + if (string.IsNullOrWhiteSpace(serverUrl) || string.IsNullOrWhiteSpace(apiKey)) + { + serverUrl = null; + apiKey = null; + return false; + } + + return true; + } + catch (Exception ex) + { + Debug.WriteLine(ex); + serverUrl = null; + apiKey = null; + return false; } } diff --git a/SldWorksLookup/Model/Instance/InstanceProperty.cs b/SldWorksLookup/Model/Instance/InstanceProperty.cs index 2e90dac..fdd3b24 100644 --- a/SldWorksLookup/Model/Instance/InstanceProperty.cs +++ b/SldWorksLookup/Model/Instance/InstanceProperty.cs @@ -2,6 +2,7 @@ using SolidWorks.Interop.sldworks; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using System.Windows; @@ -212,22 +213,35 @@ protected void GetProperties() if (IsPropertySupport(property.Name,out string msg)) { //带有索引器的属性 - if (property.GetMethod.GetParameters().Length > 0) + if ((property.GetMethod != null && property.GetMethod.GetParameters().Length > 0) || + (property.SetMethod != null && property.SetMethod.GetParameters().Length > 1)) { //生成方法 - var flag = TryMethodToLookup(property.GetMethod, Instance, out var getLookupProperty); - if (flag) + if (property.GetMethod != null) { - Properties.Add(getLookupProperty); + var flag = TryMethodToLookup(property.GetMethod, Instance, out var getLookupProperty); + if (flag) + { + Properties.Add(getLookupProperty); + } } - flag = TryMethodToLookup(property.SetMethod, Instance, out var setLookupProperty); - if (flag) + if (property.SetMethod != null) { - Properties.Add(setLookupProperty); + var flag = TryMethodToLookup(property.SetMethod, Instance, out var setLookupProperty); + if (flag) + { + Properties.Add(setLookupProperty); + } } } else//普通属性 { + if (property.GetMethod == null) + { + Properties.Add(LookupPropertyProperty.CreateMsgOnly(property, "Write-only property")); + continue; + } + var flag = TryPropertyToLookup(property, Instance, out var lookupProperty); if (flag) { @@ -265,7 +279,10 @@ protected bool TryMethodToLookup(MethodInfo method, object instance, out LookupP { lookupProperty = LookupMethodProperty.Create(method, instance); } - catch { } + catch (Exception ex) + { + Debug.WriteLine(ex); + } return lookupProperty != null; } diff --git a/SldWorksLookup/Model/Instance/MethodInstanceProperty.cs b/SldWorksLookup/Model/Instance/MethodInstanceProperty.cs index 802d1b4..2f2ea40 100644 --- a/SldWorksLookup/Model/Instance/MethodInstanceProperty.cs +++ b/SldWorksLookup/Model/Instance/MethodInstanceProperty.cs @@ -51,11 +51,28 @@ internal void Invoke() { //找到参数 var parameters = Properties.Properties.OfType(); + var methodParameters = MethodInfo.GetParameters(); - var nullParamenter = parameters.FirstOrDefault(p => p.Value == null); - if (nullParamenter != null) + var order = 0; + foreach (var parameter in parameters) { - throw new ArgumentNullException($"{nullParamenter.PropertyType.Name} is Null"); + var parameterInfo = methodParameters[order++]; + var parameterType = LookupParameterProperty.GetEffectiveType(parameterInfo.ParameterType); + if (parameter.Value == null) + { + if (parameterType.IsValueType && Nullable.GetUnderlyingType(parameterType) == null) + { + throw new ArgumentNullException($"{parameterType.Name} is Null"); + } + + continue; + } + + var validationType = Nullable.GetUnderlyingType(parameterType) ?? parameterType; + if (!validationType.IsInstanceOfType(parameter.Value)) + { + throw new ArgumentException($"{parameter.DisplayName} must be {validationType.Name}"); + } } var parametersValue = parameters.Select(p => p.Value).ToArray(); diff --git a/SldWorksLookup/Model/Property/LookupParameterProperty.cs b/SldWorksLookup/Model/Property/LookupParameterProperty.cs index 6287415..8c962d6 100644 --- a/SldWorksLookup/Model/Property/LookupParameterProperty.cs +++ b/SldWorksLookup/Model/Property/LookupParameterProperty.cs @@ -5,26 +5,30 @@ namespace SldWorksLookup.Model { public class LookupParameterProperty : LookupProperty { - public LookupParameterProperty(ParameterInfo parameter, object value) : base(parameter.Name, value, parameter.ParameterType) + public LookupParameterProperty(ParameterInfo parameter, object value) : base(parameter.Name, value, GetEffectiveType(parameter.ParameterType)) { IsReadOnly = false; } - public LookupParameterProperty(ParameterInfo parameter) : base(parameter.Name, CreateInstace(parameter.ParameterType), parameter.ParameterType) + public LookupParameterProperty(ParameterInfo parameter) : base(parameter.Name, CreateInstace(parameter.ParameterType), GetEffectiveType(parameter.ParameterType)) { IsReadOnly = false; } public static object CreateInstace(Type type) { - if (type.IsValueType) + var effectiveType = GetEffectiveType(type); + if (effectiveType.IsValueType && Nullable.GetUnderlyingType(effectiveType) == null) { - return Activator.CreateInstance(type); - } - else - { - return new object(); + return Activator.CreateInstance(effectiveType); } + + return null; + } + + internal static Type GetEffectiveType(Type type) + { + return type.IsByRef ? type.GetElementType() : type; } } diff --git a/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs b/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs index a83d5e1..5610812 100644 --- a/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs +++ b/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs @@ -13,8 +13,13 @@ public IComponent2InstanceTree(InstanceProperty instanceProperty) : base(instanc public override void AddNodesLazy() { - AddNodes(comp => comp.GetFeatures().ToArray(), - feat => $"{feat.Name}({nameof(IFeature)})"); + LazyLoadCompletion.Run( + () => + { + AddNodes(comp => comp.GetFeatures().ToArray(), + feat => $"{feat.Name}({nameof(IFeature)})"); + }, + () => NodeStatus = NodeStatus.Ok); } } -} \ No newline at end of file +} diff --git a/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs b/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs index 558fa8f..ee6e9f7 100644 --- a/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs +++ b/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs @@ -31,20 +31,25 @@ public override void AddNodesLazy() try { - if (InstanceProperty?.Instance is IFeature feat) - { - var typeName = feat.GetTypeName2(); + LazyLoadCompletion.Run( + () => + { + if (InstanceProperty?.Instance is IFeature feat) + { + var typeName = feat.GetTypeName2(); - types = TypeNameToDefinitionUtil.Match(typeName); + types = TypeNameToDefinitionUtil.Match(typeName); - foreach (var type in types) - { - AddNode(f => f.GetDefinition(), - type, _ => $"{type.Name}({typeName})"); - AddNode(f => f.GetSpecificFeature2(), - type, _ => $"{type.Name}({typeName})"); - } - } + foreach (var type in types) + { + AddNode(f => f.GetDefinition(), + type, _ => $"{type.Name}({typeName})"); + AddNode(f => f.GetSpecificFeature2(), + type, _ => $"{type.Name}({typeName})"); + } + } + }, + () => NodeStatus = NodeStatus.Ok); } catch (Exception ex) { diff --git a/SldWorksLookup/Model/Tree/LazyLoadCompletion.cs b/SldWorksLookup/Model/Tree/LazyLoadCompletion.cs new file mode 100644 index 0000000..84de5fd --- /dev/null +++ b/SldWorksLookup/Model/Tree/LazyLoadCompletion.cs @@ -0,0 +1,13 @@ +using System; + +namespace SldWorksLookup.Model +{ + public static class LazyLoadCompletion + { + public static void Run(Action load, Action markOk) + { + load(); + markOk(); + } + } +} diff --git a/SldWorksLookup/Model/Value/LookupValue.cs b/SldWorksLookup/Model/Value/LookupValue.cs index c971429..ae1d3bd 100644 --- a/SldWorksLookup/Model/Value/LookupValue.cs +++ b/SldWorksLookup/Model/Value/LookupValue.cs @@ -173,11 +173,12 @@ public bool CanSnoop if (Value is Array array) { - if (array.Length == 0) + var firstValue = array.Cast().FirstOrDefault(item => item != null); + if (firstValue == null) { return false; } - if (array.GetValue(0).GetType().IsValueType) + if (firstValue.GetType().IsValueType || firstValue is string) { return false; } @@ -247,7 +248,9 @@ private void OpenClick() } catch (Exception ex) { - ex.ToExceptionless(LogExtension.Client).Submit(); + MessageBox.Show(ExceptionUtil.GetUserMessage(ex)); + if (LogExtension.Client != null) + ex.ToExceptionless(LogExtension.Client).Submit(); } } @@ -259,6 +262,11 @@ protected void PropertySnoop() List properties = new List(); foreach (var item in array) { + if (item == null) + { + continue; + } + var reDirectType = PropertyReDirectType(item.GetType()); var ins = InstanceProperty.Create(item, reDirectType); @@ -298,7 +306,7 @@ protected void MethodSnoop() } catch (Exception ex) { - MessageBox.Show(ex.Message); + MessageBox.Show(ExceptionUtil.GetUserMessage(ex)); return; } } @@ -318,18 +326,26 @@ protected void MethodSnoop() { if (valueResult.IsValueArray()) { - var insProperties = valueResult.ObjToArray() - .Select(p => InstanceProperty.Create(p, p.GetType())) - .ToList(); - var propertyWindow = new LookupPropertyWindow(insProperties); - propertyWindow.ShowDialog(); + ValueName = $"{methodInfo.Name} => {LookupValue.CreateValue(valueResult, valueResult.GetType()).ValueName}"; } else { - var reDirectType = ReturnValueReDirectType(returnType); - var instanceProperty = InstanceProperty.Create(valueResult, reDirectType); - var propertyWindow = new LookupPropertyWindow(instanceProperty); - propertyWindow.ShowDialog(); + if (valueResult is Array array) + { + var insProperties = valueResult.ObjToArray() + .Where(p => p != null) + .Select(p => InstanceProperty.Create(p, ReturnValueReDirectType(p.GetType()))) + .ToList(); + var propertyWindow = new LookupPropertyWindow(insProperties); + propertyWindow.ShowDialog(); + } + else + { + var reDirectType = ReturnValueReDirectType(returnType); + var instanceProperty = InstanceProperty.Create(valueResult, reDirectType); + var propertyWindow = new LookupPropertyWindow(instanceProperty); + propertyWindow.ShowDialog(); + } } } } @@ -417,18 +433,22 @@ private string ToPropertyValueString() //值类型数组的显示值 if (Value is Array array) { - if (array.Length > 0) + var firstValue = array.Cast().FirstOrDefault(item => item != null); + if (firstValue == null) + { + return string.Join(",", array.Cast().Select(item => item == null ? "" : item.ToString())); + } + + var arrayItemType = firstValue.GetType(); + if (arrayItemType.IsValueType || arrayItemType == typeof(string)) { - var arrayItemType = array.GetValue(0).GetType(); - if (arrayItemType.IsValueType || arrayItemType == typeof(string)) + string strValue = string.Empty; + foreach (var item in array) { - string strValue = string.Empty; - foreach (var item in array) - { - strValue += string.IsNullOrEmpty(strValue) ? item.ToString() : $",{item.ToString()}"; - } - return strValue; + var itemValue = item == null ? "" : item.ToString(); + strValue += string.IsNullOrEmpty(strValue) ? itemValue : $",{itemValue}"; } + return strValue; } } diff --git a/SldWorksLookup/PathSplit/ExtensionMethods.cs b/SldWorksLookup/PathSplit/ExtensionMethods.cs index 7f02828..2045bbd 100644 --- a/SldWorksLookup/PathSplit/ExtensionMethods.cs +++ b/SldWorksLookup/PathSplit/ExtensionMethods.cs @@ -39,9 +39,9 @@ public static IEnumerable> GetSkeFeat(this IComponen var subFeats = feat.GetSubFeats(); foreach (var subfeat in subFeats) { - if (feat.GetTypeName2() == "ProfileFeature") + if (subfeat.GetTypeName2() == "ProfileFeature") { - yield return new Tuple(feat,comp); + yield return new Tuple(subfeat,comp); } } if (feat.GetTypeName2() == "ProfileFeature") diff --git a/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs b/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs new file mode 100644 index 0000000..09f09df --- /dev/null +++ b/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs @@ -0,0 +1,38 @@ +using System; + +namespace SldWorksLookup.PathSplit +{ + public sealed class SegmentSamplingPlan + { + private SegmentSamplingPlan(int pointCount, double firstDistance, double distanceToNextPoint) + { + PointCount = pointCount; + FirstDistance = firstDistance; + DistanceToNextPoint = distanceToNextPoint; + } + + public int PointCount { get; } + + public double FirstDistance { get; } + + public double DistanceToNextPoint { get; } + + public static SegmentSamplingPlan Create(double segmentLength, double stepLength, double distanceToNextPoint) + { + if (stepLength <= 0 || double.IsNaN(stepLength) || double.IsInfinity(stepLength)) + throw new ArgumentOutOfRangeException(nameof(stepLength)); + if (segmentLength < 0 || double.IsNaN(segmentLength) || double.IsInfinity(segmentLength)) + throw new ArgumentOutOfRangeException(nameof(segmentLength)); + if (distanceToNextPoint < 0 || double.IsNaN(distanceToNextPoint) || double.IsInfinity(distanceToNextPoint)) + throw new ArgumentOutOfRangeException(nameof(distanceToNextPoint)); + + if (distanceToNextPoint > segmentLength) + return new SegmentSamplingPlan(0, 0, distanceToNextPoint - segmentLength); + + var count = (int)Math.Floor((segmentLength - distanceToNextPoint) / stepLength) + 1; + var lastDistance = distanceToNextPoint + (count - 1) * stepLength; + var nextDistance = stepLength - (segmentLength - lastDistance); + return new SegmentSamplingPlan(count, distanceToNextPoint, nextDistance); + } + } +} diff --git a/SldWorksLookup/PathSplit/SketchChain.cs b/SldWorksLookup/PathSplit/SketchChain.cs index c7b1edc..4383921 100644 --- a/SldWorksLookup/PathSplit/SketchChain.cs +++ b/SldWorksLookup/PathSplit/SketchChain.cs @@ -71,6 +71,9 @@ public double GetLength() /// public List Split(double stepLength) { + if (stepLength <= 0 || double.IsNaN(stepLength) || double.IsInfinity(stepLength)) + throw new ArgumentOutOfRangeException(nameof(stepLength)); + //初始化列表 List points = new List(); @@ -78,8 +81,6 @@ public List Split(double stepLength) double spareLength = 0; foreach (var seg in Segs) { - var length = seg.Segment.GetLength(); - //是否有上根线段的剩余长度 var curvePts = seg.SplitSegment(stepLength, spareLength,out var nextSpareLength); diff --git a/SldWorksLookup/PathSplit/SketchChainTopology.cs b/SldWorksLookup/PathSplit/SketchChainTopology.cs new file mode 100644 index 0000000..4fb1ebf --- /dev/null +++ b/SldWorksLookup/PathSplit/SketchChainTopology.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Media.Media3D; + +namespace SldWorksLookup.PathSplit +{ + public static class SketchChainTopology + { + public static List> Build( + IList segments, + Func getStartPoint, + Func getEndPoint, + Action reverse, + Func pointsEqual) + where T : class + { + if (segments == null) + throw new ArgumentNullException(nameof(segments)); + if (getStartPoint == null) + throw new ArgumentNullException(nameof(getStartPoint)); + if (getEndPoint == null) + throw new ArgumentNullException(nameof(getEndPoint)); + if (reverse == null) + throw new ArgumentNullException(nameof(reverse)); + if (pointsEqual == null) + throw new ArgumentNullException(nameof(pointsEqual)); + + var chains = new List>(); + var remaining = new List(segments); + + while (remaining.Count > 0) + { + var firstIndex = FindOpenEndpointSegmentIndex(remaining, getStartPoint, getEndPoint, pointsEqual); + if (firstIndex < 0) + firstIndex = 0; + + var first = remaining[firstIndex]; + var startConnected = IsPointConnected(first, getStartPoint(first), remaining, getStartPoint, getEndPoint, pointsEqual); + var endConnected = IsPointConnected(first, getEndPoint(first), remaining, getStartPoint, getEndPoint, pointsEqual); + + if (startConnected && !endConnected) + reverse(first); + + remaining.RemoveAt(firstIndex); + + var chain = new List { first }; + ConsumeConnectedSegments(chain, remaining, getStartPoint, getEndPoint, reverse, pointsEqual); + chains.Add(chain); + } + + return chains; + } + + private static int FindOpenEndpointSegmentIndex( + List segments, + Func getStartPoint, + Func getEndPoint, + Func pointsEqual) + where T : class + { + for (var i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + var startConnected = IsPointConnected(segment, getStartPoint(segment), segments, getStartPoint, getEndPoint, pointsEqual); + var endConnected = IsPointConnected(segment, getEndPoint(segment), segments, getStartPoint, getEndPoint, pointsEqual); + + if (!startConnected || !endConnected) + return i; + } + + return -1; + } + + private static bool IsPointConnected( + T source, + Point3D point, + List segments, + Func getStartPoint, + Func getEndPoint, + Func pointsEqual) + where T : class + { + foreach (var segment in segments) + { + if (ReferenceEquals(source, segment)) + continue; + + if (pointsEqual(point, getStartPoint(segment)) || pointsEqual(point, getEndPoint(segment))) + return true; + } + + return false; + } + + private static void ConsumeConnectedSegments( + List chain, + List remaining, + Func getStartPoint, + Func getEndPoint, + Action reverse, + Func pointsEqual) + where T : class + { + while (remaining.Count > 0) + { + var currentEnd = getEndPoint(chain.Last()); + var nextIndex = -1; + var reverseNext = false; + + for (var i = 0; i < remaining.Count; i++) + { + if (pointsEqual(currentEnd, getStartPoint(remaining[i]))) + { + nextIndex = i; + break; + } + + if (pointsEqual(currentEnd, getEndPoint(remaining[i]))) + { + nextIndex = i; + reverseNext = true; + break; + } + } + + if (nextIndex < 0) + return; + + var next = remaining[nextIndex]; + if (reverseNext) + reverse(next); + + remaining.RemoveAt(nextIndex); + chain.Add(next); + } + } + } +} diff --git a/SldWorksLookup/PathSplit/SketchSegmentWrapper.cs b/SldWorksLookup/PathSplit/SketchSegmentWrapper.cs index 412c63f..eb89d8b 100644 --- a/SldWorksLookup/PathSplit/SketchSegmentWrapper.cs +++ b/SldWorksLookup/PathSplit/SketchSegmentWrapper.cs @@ -104,11 +104,6 @@ public bool IsEndConnected(SketchSegmentWrapper segment) /// 分割后的点 public List SplitSegment(double stepLength, double spareLength ,out double newSpareLength) { - if (stepLength > GetLength()) - { - throw new InvalidOperationException($"步长:{stepLength} 大于 当前草图元素长度"); - } - //获取草图线段曲线 var seCurve = Segment.GetCurve() as ICurve; @@ -131,61 +126,23 @@ private List SplitCurve(ICurve curve, double stepLength, double spareLe { //初始化点几何 var points = new List(); + var length = GetLength(); + var plan = SegmentSamplingPlan.Create(length, stepLength, spareLength); //获取曲线元素 curve.GetEndParams(out var startParam, out var endParam, out bool isClosed, out bool isPeriodic); - if (ReverseSpAndEp) - { - //数量,取整了 - int num = (int)((GetLength() - spareLength )/ stepLength); - - //步长参数 - var incr = (endParam - startParam) / (num); - - for (int i = 0; i < num; i++) - { - var param = curve.Evaluate(startParam + i * incr) as double[]; - points.Add(new Point3D(param[0], param[1], param[2])); - } - - if (spareLength < ExtensionMethods.Eplision) - { - //第一个点参数 - var lastParam = endParam - ((endParam - startParam) / (GetLength() / spareLength)); - var lastPoint = curve.Evaluate(lastParam) as double[]; - points.Add(lastPoint.ToPoint()); - } - - //逆序 - points.Reverse(); - - newSpareLength = GetLength() - spareLength - stepLength * num; - } - else + for (var i = 0; i < plan.PointCount; i++) { - //第一个点参数 - var firstParam = ((endParam - startParam) / (GetLength() / spareLength)) + startParam; - - //数量,取整了 - int num = (int)(GetLength() / stepLength); - - //步长参数 - var incr = (endParam - firstParam) / (num); - - for (int i = 0; i < num; i++) - { - var param = curve.Evaluate(firstParam + i * incr) as double[]; - points.Add(new Point3D(param[0], param[1], param[2])); - } - - newSpareLength = GetLength() - spareLength - stepLength * num; + var distance = plan.FirstDistance + i * stepLength; + var ratio = length < ExtensionMethods.Eplision ? 0 : distance / length; + var curveRatio = ReverseSpAndEp ? 1 - ratio : ratio; + var curveParameter = startParam + (endParam - startParam) * curveRatio; + var point = curve.Evaluate(curveParameter) as double[]; + points.Add(point.ToPoint()); } - if (newSpareLength < stepLength) - { - newSpareLength = stepLength - newSpareLength; - } + newSpareLength = plan.DistanceToNextPoint; return points; } diff --git a/SldWorksLookup/PathSplit/SketchWrapper.cs b/SldWorksLookup/PathSplit/SketchWrapper.cs index dded0ad..1ea5bb5 100644 --- a/SldWorksLookup/PathSplit/SketchWrapper.cs +++ b/SldWorksLookup/PathSplit/SketchWrapper.cs @@ -31,65 +31,25 @@ public SketchWrapper(IFeature feat, IComponent2 comp) #region Public Methods public IEnumerable GetChains() { - var ses = (_sketch.GetSketchSegments() as object[]) + var sketchSegments = _sketch.GetSketchSegments() as object[]; + if (sketchSegments == null) + yield break; + + var ses = sketchSegments .Cast() .Select(p => new SketchSegmentWrapper(p)) .ToList(); - //挑选出起点 - for (int i = 0; i < ses.Count; i++) - { - //判断当前直线是否可以作为路径其实直线 - bool startConnected = false; - bool endConnected = false; - for (int j = 0; j < ses.Count; j++) - { - if (i == j) - continue; - - if(!startConnected) - startConnected = ses[i].IsStartConnected(ses[j]); - if (!endConnected) - endConnected = ses[i].IsEndConnected(ses[j]); - - if (startConnected && endConnected) - break; - } - - if (!startConnected || !endConnected) - { - //起点和其他直线相连,交换起点和终点 - if (startConnected) - ses[i].ReverseSpAndEp = true; - - var chainSes = new List() { ses[i] }; - ses.RemoveAt(i); - - //构建新列表 - var newSes = new List(); - newSes.AddRange(ses); - - //寻找其他相连直线 - var next = chainSes.Last(); - - //递归查找 - while (next != null) - { - next = SearchNext(chainSes, next, newSes); - } + var chains = SketchChainTopology.Build( + ses, + segment => segment.StartPoint, + segment => segment.EndPoint, + segment => segment.ReverseSpAndEp = !segment.ReverseSpAndEp, + (left, right) => left.ValueEqual(right)); - //从现有列表中剔除 - foreach (var usedSe in chainSes) - { - ses.Remove(usedSe); - } - - //重新查找 - i = 0; - - //返回草图链条 - yield return new SketchChain(_sketch,chainSes,_comp); - } + foreach (var chain in chains) + { + yield return new SketchChain(_sketch, chain, _comp); } } @@ -99,31 +59,5 @@ public override string ToString() } #endregion - #region Private Methods - private static SketchSegmentWrapper SearchNext(List chainSes, SketchSegmentWrapper current, List newSes) - { - var next = default(SketchSegmentWrapper); - for (int k = 0; k < newSes.Count; k++) - { - if (current.EndPoint.ValueEqual(newSes[k].StartPoint)) - { - chainSes.Add(newSes[k]); - newSes.RemoveAt(k--); - next = chainSes.Last(); - break; - } - else if (current.EndPoint.ValueEqual(newSes[k].EndPoint)) - { - newSes[k].ReverseSpAndEp = true; - chainSes.Add(newSes[k]); - newSes.RemoveAt(k--); - next = chainSes.Last(); - break; - } - } - return next; - } - #endregion - } } diff --git a/SldWorksLookup/Properties/AssemblyInfo.cs b/SldWorksLookup/Properties/AssemblyInfo.cs index 8b6feeb..b8d3204 100644 --- a/SldWorksLookup/Properties/AssemblyInfo.cs +++ b/SldWorksLookup/Properties/AssemblyInfo.cs @@ -13,6 +13,7 @@ [assembly: AssemblyCopyright("Copyright © 2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] +[assembly: InternalsVisibleTo("SldWorksLookup.RegressionTests")] // 将 ComVisible 设置为 false 会使此程序集中的类型 //对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 diff --git a/SldWorksLookup/SldWorksLookup.csproj b/SldWorksLookup/SldWorksLookup.csproj index b123393..7d704f0 100644 --- a/SldWorksLookup/SldWorksLookup.csproj +++ b/SldWorksLookup/SldWorksLookup.csproj @@ -10,6 +10,7 @@ SldWorksLookup SldWorksLookup v4.7.2 + false 512 true @@ -28,10 +29,11 @@ pdbonly true - bin\Release\ + ..\bin\ TRACE prompt 4 + x64 true @@ -169,6 +171,10 @@ + + + + True True @@ -194,6 +200,7 @@ + @@ -216,6 +223,8 @@ + + @@ -337,4 +346,4 @@ - \ No newline at end of file + diff --git a/SldWorksLookup/UnInstall.bat b/SldWorksLookup/UnInstall.bat index 8ea76e2..0bf16eb 100644 --- a/SldWorksLookup/UnInstall.bat +++ b/SldWorksLookup/UnInstall.bat @@ -1,5 +1,8 @@ -set path=%~d0 -cd %path% -cd /d %~dp0 - -RegAsm.exe SldWorksLookup.dll /u +@echo off +setlocal +cd /d "%~dp0" || exit /b 1 +if not exist "%~dp0RegAsm.exe" exit /b 2 +if not exist "%~dp0SldWorksLookup.dll" exit /b 3 +"%~dp0RegAsm.exe" "%~dp0SldWorksLookup.dll" /u +if errorlevel 1 exit /b %errorlevel% +exit /b 0 diff --git a/SldWorksLookup/View/CaptureCmd.xaml.cs b/SldWorksLookup/View/CaptureCmd.xaml.cs index 53c01a8..054e43c 100644 --- a/SldWorksLookup/View/CaptureCmd.xaml.cs +++ b/SldWorksLookup/View/CaptureCmd.xaml.cs @@ -1,4 +1,5 @@ -using SldWorksLookup.ViewModel; +using SldWorksLookup.Helper; +using SldWorksLookup.ViewModel; using System; using System.Windows; using System.Windows.Interop; @@ -20,21 +21,26 @@ public CaptureCmd(SwApplication app) InitializeComponent(); _viewModel = new CaptureCmdViewModel(_app); - _viewModel.CloseAction = new Action(() => this.Close()); + ConstructionCleanup.Run( + () => + { + _viewModel.CloseAction = new Action(() => this.Close()); - var interopHelper = new WindowInteropHelper(this); - interopHelper.Owner = _app.WindowHandle; + var interopHelper = new WindowInteropHelper(this); + interopHelper.Owner = _app.WindowHandle; - this.Closed += CaptureCmd_Closed; + this.Closed += CaptureCmd_Closed; - DataContext = _viewModel; + DataContext = _viewModel; + }, + () => _viewModel.Dispose()); } private void CaptureCmd_Closed(object sender, EventArgs e) { this.Closed -= CaptureCmd_Closed; - _viewModel.DeAttachEvent(); + _viewModel.Dispose(); } } } diff --git a/SldWorksLookup/ViewModel/CaptureCmdViewModel.cs b/SldWorksLookup/ViewModel/CaptureCmdViewModel.cs index ee31f43..1db1339 100644 --- a/SldWorksLookup/ViewModel/CaptureCmdViewModel.cs +++ b/SldWorksLookup/ViewModel/CaptureCmdViewModel.cs @@ -9,11 +9,12 @@ namespace SldWorksLookup.ViewModel { - public class CaptureCmdViewModel : ViewModelBase + public class CaptureCmdViewModel : ViewModelBase, IDisposable { private SwApplication _app; private SldWorks _sw; private RelayCommand _closeCommand; + private bool _disposed; public CaptureCmdViewModel(SwApplication app) { @@ -36,8 +37,12 @@ private int _sw_CommandOpenPreNotify(int Command, int UserCommand) return 0; } - internal void DeAttachEvent() + public void Dispose() { + if (_disposed) + return; + + _disposed = true; _sw.CommandOpenPreNotify -= _sw_CommandOpenPreNotify; } } diff --git a/SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs b/SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs index 6ac73fd..b580cc9 100644 --- a/SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs +++ b/SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs @@ -5,6 +5,7 @@ using SolidWorks.Interop.swconst; using System; using Xarial.XCad; +using SldWorksLookup.Helper; namespace SldWorksLookup.View { @@ -61,7 +62,7 @@ private void GetObjectClick() } catch (Exception ex) { - _application.ShowMessageBox(ex.Message); + _application.ShowMessageBox(ExceptionUtil.GetUserMessage(ex)); } } diff --git a/tests/SldWorksLookup.RegressionTests/Program.cs b/tests/SldWorksLookup.RegressionTests/Program.cs new file mode 100644 index 0000000..c5f9702 --- /dev/null +++ b/tests/SldWorksLookup.RegressionTests/Program.cs @@ -0,0 +1,565 @@ +using SldWorksLookup.PathSplit; +using SldWorksLookup.Helper; +using SldWorksLookup.Model; +using SolidWorks.Interop.sldworks; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows.Media.Media3D; + +namespace SldWorksLookup.RegressionTests +{ + internal static class Program + { + private static int Main() + { + var tests = new Action[] + { + PathTopologyReturnsEveryDisconnectedSegment, + PathTopologyReturnsClosedLoop, + PathTopologyReversesOpenFirstSegment, + PathTopologyReversesConnectedSuccessor, + PathTopologyKeepsEveryStepContinuous, + PathTopologyDoesNotReorderOrRemoveInputSegments, + SamplingPlanCarriesSpacingAcrossShortSegment, + SamplingPlanRejectsNonPositiveOrNonFiniteStep, + SamplingPlanRejectsInvalidCarryDistance, + SamplingPlanReturnsNormalSpacing, + SamplingPlanHandlesExactCarryBoundary, + ExportMergeFailureThrowsContext, + EditScopeExitsOnceAndRethrowsBodyException, + EditScopeExitsOnceAfterSuccess, + SelectionAccessScopeReleasesWhenBodyThrows, + SelectionAccessScopeDoesNotRunBodyWhenAcquireFails, + ExceptionUtilUnwrapsTargetInvocationException, + LogConfigurationReadsTwoTrimmedValues, + LogConfigurationRejectsMissingOrIncompleteFile, + ConstructionCleanupRunsCleanupOnceWhenInitializeThrows, + ConstructionCleanupDoesNotCleanupWhenInitializeSucceeds, + ValueArrayInspectionHandlesNullElements, + PropertyBrowsingHandlesSetterOnlyProperty, + ReferenceParametersDefaultToNull, + ComClassWithInternalIMapsToInterface, + LazyLoadCompletionMarksOkAfterSuccess, + LazyLoadCompletionLeavesNeedRunWhenLoadThrows, + TypeMatcherGeneratedEntriesUseInterfacesWithSingleLeadingI, + ValueArrayInspectionDisplaysAllNullElements + }; + + var failed = 0; + foreach (var test in tests) + { + try + { + test(); + Console.WriteLine("PASS " + test.Method.Name); + } + catch (Exception ex) + { + failed++; + Console.WriteLine("FAIL " + test.Method.Name); + Console.WriteLine(ex.GetType().Name + ": " + ex.Message); + } + } + + return failed == 0 ? 0 : 1; + } + + private static void PathTopologyReturnsEveryDisconnectedSegment() + { + var segments = new List + { + new Segment(Point(0, 0), Point(1, 0)), + new Segment(Point(2, 0), Point(3, 0)) + }; + + var chains = BuildChains(segments); + + AssertEqual(2, chains.Count, "Disconnected segment count"); + AssertEqual(1, chains[0].Count, "First chain segment count"); + AssertEqual(1, chains[1].Count, "Second chain segment count"); + AssertSame(segments[0], chains[0][0], "First chain segment"); + AssertSame(segments[1], chains[1][0], "Second chain segment"); + } + + private static void PathTopologyReturnsClosedLoop() + { + var segments = new List + { + new Segment(Point(0, 0), Point(1, 0)), + new Segment(Point(1, 0), Point(1, 1)), + new Segment(Point(1, 1), Point(0, 0)) + }; + + var chains = BuildChains(segments); + + AssertEqual(1, chains.Count, "Closed loop chain count"); + AssertEqual(3, chains[0].Count, "Closed loop segment count"); + AssertPointEqual(chains[0][0].Start, chains[0][2].End, "Closed loop endpoints"); + } + + private static void SamplingPlanCarriesSpacingAcrossShortSegment() + { + var plan = SegmentSamplingPlan.Create(0.5, 1.0, 0.75); + + AssertEqual(0, plan.PointCount, "Point count"); + AssertClose(0, plan.FirstDistance, "First distance"); + AssertClose(0.25, plan.DistanceToNextPoint, "Distance to next point"); + } + + private static void PathTopologyReversesOpenFirstSegment() + { + var first = new Segment(Point(1, 0), Point(0, 0)); + var second = new Segment(Point(1, 0), Point(2, 0)); + var segments = new List { first, second }; + + var chains = BuildChains(segments); + + AssertEqual(1, chains.Count, "Chain count"); + AssertSame(first, chains[0][0], "First segment instance"); + AssertPointEqual(Point(0, 0), chains[0][0].Start, "Reversed first start"); + AssertPointEqual(Point(1, 0), chains[0][0].End, "Reversed first end"); + AssertPointEqual(chains[0][0].End, chains[0][1].Start, "First-to-second continuity"); + } + + private static void PathTopologyReversesConnectedSuccessor() + { + var first = new Segment(Point(0, 0), Point(1, 0)); + var second = new Segment(Point(2, 0), Point(1, 0)); + var segments = new List { first, second }; + + var chains = BuildChains(segments); + + AssertEqual(1, chains.Count, "Chain count"); + AssertSame(second, chains[0][1], "Second segment instance"); + AssertPointEqual(Point(1, 0), chains[0][1].Start, "Reversed successor start"); + AssertPointEqual(Point(2, 0), chains[0][1].End, "Reversed successor end"); + AssertPointEqual(chains[0][0].End, chains[0][1].Start, "Successor continuity"); + } + + private static void PathTopologyKeepsEveryStepContinuous() + { + var segments = new List + { + new Segment(Point(3, 0), Point(2, 0)), + new Segment(Point(0, 0), Point(1, 0)), + new Segment(Point(2, 0), Point(1, 0)), + new Segment(Point(3, 0), Point(4, 0)) + }; + + var chains = BuildChains(segments); + + AssertEqual(1, chains.Count, "Chain count"); + for (var i = 1; i < chains[0].Count; i++) + { + AssertPointEqual(chains[0][i - 1].End, chains[0][i].Start, "Continuity at segment " + i); + } + } + + private static void PathTopologyDoesNotReorderOrRemoveInputSegments() + { + var first = new Segment(Point(1, 0), Point(0, 0)); + var second = new Segment(Point(1, 0), Point(2, 0)); + var third = new Segment(Point(3, 0), Point(4, 0)); + var segments = new List { first, second, third }; + + BuildChains(segments); + + AssertEqual(3, segments.Count, "Input segment count"); + AssertSame(first, segments[0], "Input first segment"); + AssertSame(second, segments[1], "Input second segment"); + AssertSame(third, segments[2], "Input third segment"); + } + + private static void SamplingPlanRejectsNonPositiveOrNonFiniteStep() + { + AssertThrows(() => SegmentSamplingPlan.Create(1.0, 0, 0), "Zero step"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, -1.0, 0), "Negative step"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, double.NaN, 0), "NaN step"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, double.PositiveInfinity, 0), "Infinite step"); + } + + private static void SamplingPlanRejectsInvalidCarryDistance() + { + AssertThrows(() => SegmentSamplingPlan.Create(1.0, 1.0, -0.1), "Negative carry distance"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, 1.0, double.NaN), "NaN carry distance"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, 1.0, double.PositiveInfinity), "Infinite carry distance"); + } + + private static void SamplingPlanReturnsNormalSpacing() + { + var plan = SegmentSamplingPlan.Create(5.0, 2.0, 1.0); + + AssertEqual(3, plan.PointCount, "Point count"); + AssertClose(1.0, plan.FirstDistance, "First distance"); + AssertClose(2.0, plan.DistanceToNextPoint, "Distance to next point"); + } + + private static void SamplingPlanHandlesExactCarryBoundary() + { + var plan = SegmentSamplingPlan.Create(3.0, 2.0, 3.0); + + AssertEqual(1, plan.PointCount, "Point count"); + AssertClose(3.0, plan.FirstDistance, "First distance"); + AssertClose(2.0, plan.DistanceToNextPoint, "Distance to next point"); + } + + private static void ExportMergeFailureThrowsContext() + { + var existing = new CurveToken("existing"); + var next = new CurveToken("next"); + + var first = PathExportUtil.MergeCurveOrThrow(null, next, (left, right) => new CurveToken("unused"), "segment 1"); + AssertSame(next, first, "First curve should initialize export state"); + + var ex = AssertThrows( + () => PathExportUtil.MergeCurveOrThrow(existing, next, (left, right) => null, "segment 2"), + "Merge failure"); + + if (!ex.Message.Contains("segment 2")) + throw new InvalidOperationException("Merge failure should include context. Message: " + ex.Message); + } + + private static void EditScopeExitsOnceAndRethrowsBodyException() + { + var enterCount = 0; + var exitCount = 0; + var expected = new InvalidOperationException("body failed"); + + var actual = AssertThrows( + () => EditScope.Run( + () => enterCount++, + () => exitCount++, + () => { throw expected; }), + "Body failure"); + + AssertSame(expected, actual, "Original exception"); + AssertEqual(1, enterCount, "Enter count"); + AssertEqual(1, exitCount, "Exit count"); + } + + private static void EditScopeExitsOnceAfterSuccess() + { + var enterCount = 0; + var bodyCount = 0; + var exitCount = 0; + + EditScope.Run( + () => enterCount++, + () => exitCount++, + () => bodyCount++); + + AssertEqual(1, enterCount, "Enter count"); + AssertEqual(1, bodyCount, "Body count"); + AssertEqual(1, exitCount, "Exit count"); + } + + private static void SelectionAccessScopeReleasesWhenBodyThrows() + { + var releaseCount = 0; + var ex = AssertThrows( + () => SelectionAccessScope.Run( + () => true, + () => releaseCount++, + () => { throw new InvalidOperationException("body failed"); }), + "Body failure"); + + AssertEqual("body failed", ex.Message, "Body exception message"); + AssertEqual(1, releaseCount, "Release count"); + } + + private static void SelectionAccessScopeDoesNotRunBodyWhenAcquireFails() + { + var bodyRunCount = 0; + var releaseCount = 0; + + AssertThrows( + () => SelectionAccessScope.Run( + () => false, + () => releaseCount++, + () => bodyRunCount++), + "Acquire failure"); + + AssertEqual(0, bodyRunCount, "Body run count"); + AssertEqual(0, releaseCount, "Release count"); + } + + private static void ExceptionUtilUnwrapsTargetInvocationException() + { + var inner = new InvalidOperationException("SOLIDWORKS refused the command"); + var outer = new TargetInvocationException(inner); + + AssertEqual("SOLIDWORKS refused the command", ExceptionUtil.GetUserMessage(outer), "User message"); + } + + private static void LogConfigurationReadsTwoTrimmedValues() + { + var path = Path.GetTempFileName(); + try + { + File.WriteAllLines(path, new[] { " https://logs.example.test ", " api-key " }); + + string serverUrl; + string apiKey; + var result = LogExtension.TryReadConfiguration(path, out serverUrl, out apiKey); + + AssertEqual(true, result, "Configuration result"); + AssertEqual("https://logs.example.test", serverUrl, "Server URL"); + AssertEqual("api-key", apiKey, "API key"); + } + finally + { + File.Delete(path); + } + } + + private static void LogConfigurationRejectsMissingOrIncompleteFile() + { + string serverUrl; + string apiKey; + var missingPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".txt"); + AssertEqual(false, LogExtension.TryReadConfiguration(missingPath, out serverUrl, out apiKey), "Missing file"); + + var path = Path.GetTempFileName(); + try + { + File.WriteAllLines(path, new[] { "https://logs.example.test" }); + AssertEqual(false, LogExtension.TryReadConfiguration(path, out serverUrl, out apiKey), "Incomplete file"); + + File.WriteAllLines(path, new[] { "https://logs.example.test", " " }); + AssertEqual(false, LogExtension.TryReadConfiguration(path, out serverUrl, out apiKey), "Blank api key"); + } + finally + { + File.Delete(path); + } + } + + private static void ConstructionCleanupRunsCleanupOnceWhenInitializeThrows() + { + var cleanupCount = 0; + var expected = new InvalidOperationException("owner failed"); + + var actual = AssertThrows( + () => ConstructionCleanup.Run( + () => { throw expected; }, + () => cleanupCount++), + "Initialize failure"); + + AssertSame(expected, actual, "Original exception"); + AssertEqual(1, cleanupCount, "Cleanup count"); + } + + private static void ConstructionCleanupDoesNotCleanupWhenInitializeSucceeds() + { + var cleanupCount = 0; + var initializeCount = 0; + + ConstructionCleanup.Run( + () => initializeCount++, + () => cleanupCount++); + + AssertEqual(1, initializeCount, "Initialize count"); + AssertEqual(0, cleanupCount, "Cleanup count"); + } + + private static void ValueArrayInspectionHandlesNullElements() + { + var values = new object[] { null, 42 }; + + AssertEqual(true, ObjectMatcherUtil.IsValueArray(values), "Null-leading value array"); + + var lookup = LookupValue.CreateValue(values, typeof(object[])); + AssertEqual(",42", lookup.ValueName, "Null-leading value array display"); + } + + private static void PropertyBrowsingHandlesSetterOnlyProperty() + { + var instanceProperty = InstanceProperty.Create(new SetterOnlyPropertyOwner(), typeof(SetterOnlyPropertyOwner)); + var property = instanceProperty.Properties.Properties + .FirstOrDefault(p => p.DisplayName == nameof(SetterOnlyPropertyOwner.WriteOnly)); + + if (property == null) + throw new InvalidOperationException("Setter-only property was not surfaced."); + + AssertEqual("Write-only property", property.Value as string, "Setter-only property message"); + } + + private static void ReferenceParametersDefaultToNull() + { + AssertEqual(null, LookupParameterProperty.CreateInstace(typeof(string)), "String default"); + } + + private static void ComClassWithInternalIMapsToInterface() + { + AssertSame(typeof(IImportDxfDwgData), TypeMatcherUtil.Match(typeof(ImportDxfDwgDataClass)), "Import DXF/DWG interface"); + } + + private static void LazyLoadCompletionMarksOkAfterSuccess() + { + var nodeStatus = NodeStatus.NeedRun; + var loadCount = 0; + + LazyLoadCompletion.Run( + () => loadCount++, + () => nodeStatus = NodeStatus.Ok); + + AssertEqual(1, loadCount, "Load count"); + AssertEqual(NodeStatus.Ok, nodeStatus, "Node status"); + } + + private static void LazyLoadCompletionLeavesNeedRunWhenLoadThrows() + { + var nodeStatus = NodeStatus.NeedRun; + var markOkCount = 0; + + AssertThrows( + () => LazyLoadCompletion.Run( + () => { throw new InvalidOperationException("load failed"); }, + () => + { + markOkCount++; + nodeStatus = NodeStatus.Ok; + }), + "Lazy load failure"); + + AssertEqual(0, markOkCount, "Mark OK count"); + AssertEqual(NodeStatus.NeedRun, nodeStatus, "Node status"); + } + + private static void TypeMatcherGeneratedEntriesUseInterfacesWithSingleLeadingI() + { + var errors = TypeMatcherUtil.SolidWorksTypes + .Where(tuple => !tuple.Item2.IsInterface || tuple.Item1 != tuple.Item2.Name.Substring(1)) + .Select(tuple => tuple.Item1 + " => " + tuple.Item2.Name) + .ToArray(); + + if (errors.Length > 0) + throw new InvalidOperationException("Invalid TypeMatcher entries: " + string.Join("; ", errors)); + } + + private static void ValueArrayInspectionDisplaysAllNullElements() + { + var values = new object[] { null, null }; + var lookup = LookupValue.CreateValue(values, typeof(object[])); + + AssertEqual(",", lookup.ValueName, "All-null array display"); + } + + private static List> BuildChains(List segments) + { + return SketchChainTopology.Build( + segments, + segment => segment.Start, + segment => segment.End, + segment => segment.Reverse(), + (left, right) => left.ValueEqual(right)).ToList(); + } + + private static Point3D Point(double x, double y) + { + return new Point3D(x, y, 0); + } + + private static void AssertEqual(int expected, int actual, string message) + { + if (expected != actual) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static void AssertEqual(bool expected, bool actual, string message) + { + if (expected != actual) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static void AssertEqual(string expected, string actual, string message) + { + if (expected != actual) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static void AssertEqual(object expected, object actual, string message) + { + if (!object.Equals(expected, actual)) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static void AssertSame(object expected, object actual, string message) + { + if (!ReferenceEquals(expected, actual)) + throw new InvalidOperationException(message + ". Expected same instance."); + } + + private static void AssertPointEqual(Point3D expected, Point3D actual, string message) + { + if (!expected.ValueEqual(actual)) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static void AssertClose(double expected, double actual, string message) + { + if (Math.Abs(expected - actual) > ExtensionMethods.Eplision) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static TException AssertThrows(Action action, string message) + where TException : Exception + { + try + { + action(); + } + catch (TException ex) + { + return ex; + } + catch (Exception ex) + { + throw new InvalidOperationException(message + ". Expected " + typeof(TException).Name + ", got " + ex.GetType().Name + "."); + } + + throw new InvalidOperationException(message + ". Expected " + typeof(TException).Name + "."); + } + + private sealed class CurveToken + { + public CurveToken(string name) + { + Name = name; + } + + public string Name { get; private set; } + } + + private sealed class Segment + { + public Segment(Point3D start, Point3D end) + { + Start = start; + End = end; + } + + public Point3D Start { get; private set; } + + public Point3D End { get; private set; } + + public void Reverse() + { + var start = Start; + Start = End; + End = start; + } + } + + private sealed class SetterOnlyPropertyOwner + { + public int WriteOnly + { + set { } + } + } + } +} diff --git a/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj b/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj new file mode 100644 index 0000000..49f62d9 --- /dev/null +++ b/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj @@ -0,0 +1,23 @@ + + + Exe + net472 + 7.3 + x64 + + + + ..\..\packages\MvvmLightLibs.5.4.1.1\lib\net45\GalaSoft.MvvmLight.dll + + + + + ..\..\packages\Xarial.XCad.SolidWorks.Interops.0.2.0\lib\net40\SolidWorks.Interop.sldworks.dll + False + + + + + + + diff --git a/tests/Verify-ProjectConfiguration.ps1 b/tests/Verify-ProjectConfiguration.ps1 new file mode 100644 index 0000000..5ecc5c0 --- /dev/null +++ b/tests/Verify-ProjectConfiguration.ps1 @@ -0,0 +1,88 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot + +function Read-RepoFile([string]$relativePath) { + return Get-Content -Raw -LiteralPath (Join-Path $repoRoot $relativePath) +} + +function Assert-Matches([string]$text, [string]$pattern, [string]$message) { + if ($text -notmatch $pattern) { + throw $message + } +} + +function Assert-NotMatches([string]$text, [string]$pattern, [string]$message) { + if ($text -match $pattern) { + throw $message + } +} + +function Assert-XmlFile([string]$relativePath) { + $xml = New-Object System.Xml.XmlDocument + $xml.PreserveWhitespace = $true + $xml.Load((Join-Path $repoRoot $relativePath)) + return $xml +} + +function Assert-ProjectProperty([string]$relativePath, [string]$condition, [string]$propertyName, [string]$expectedValue, [string]$message) { + $xml = Assert-XmlFile $relativePath + $namespaceManager = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) + $namespaceManager.AddNamespace('msb', 'http://schemas.microsoft.com/developer/msbuild/2003') + + $propertyGroup = $xml.SelectSingleNode("//msb:PropertyGroup[@Condition=`"$condition`"]", $namespaceManager) + if ($null -eq $propertyGroup) { + throw "Missing project property group: $condition" + } + + $property = $propertyGroup.SelectSingleNode("msb:$propertyName", $namespaceManager) + if ($null -eq $property -or $property.InnerText -ne $expectedValue) { + throw $message + } +} + +$solution = Read-RepoFile 'SldWorksLookup.sln' +$project = Read-RepoFile 'SldWorksLookup\SldWorksLookup.csproj' +$testsProject = Read-RepoFile 'tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj' +$install = Read-RepoFile 'SldWorksLookup\Install.bat' +$uninstall = Read-RepoFile 'SldWorksLookup\UnInstall.bat' +$installer = Read-RepoFile 'Installer\SolidWorksLookup.aip' + +Assert-Matches $solution 'Release\|Any CPU\.ActiveCfg = Release\|Any CPU' 'Release|Any CPU must map to project Release|Any CPU.' +Assert-Matches $solution 'Release\|x64\.ActiveCfg = Release\|x64' 'Release|x64 must map to project Release|x64.' +Assert-NotMatches $solution 'Release\|Any CPU\.(ActiveCfg|Build\.0) = Debug\|' 'Release|Any CPU must not build a Debug project configuration.' +Assert-NotMatches $solution 'Release\|x64\.(ActiveCfg|Build\.0) = Debug\|' 'Release|x64 must not build a Debug project configuration.' + +Assert-Matches $project 'false' 'XCadRegDll must default to false while allowing command-line overrides.' +Assert-ProjectProperty 'SldWorksLookup\SldWorksLookup.csproj' ' ''$(Configuration)|$(Platform)'' == ''Release|AnyCPU'' ' 'OutputPath' '..\bin\' 'Release|AnyCPU output must refresh the root bin directory used by the installer.' + +Assert-Matches $install 'cd /d "%~dp0" \|\| exit /b 1' 'Install.bat must run from its own directory.' +Assert-Matches $install 'if not exist "%~dp0RegAsm\.exe" exit /b 2' 'Install.bat must verify RegAsm.exe exists.' +Assert-Matches $install 'if not exist "%~dp0SldWorksLookup\.dll" exit /b 3' 'Install.bat must verify SldWorksLookup.dll exists.' +Assert-Matches $install '"%~dp0RegAsm\.exe" "%~dp0SldWorksLookup\.dll" /codebase' 'Install.bat must quote RegAsm and target paths.' +Assert-Matches $install 'if errorlevel 1 exit /b %errorlevel%' 'Install.bat must propagate RegAsm failures.' +Assert-Matches $install 'exit /b 0' 'Install.bat must return 0 only after success.' + +Assert-Matches $uninstall 'cd /d "%~dp0" \|\| exit /b 1' 'UnInstall.bat must run from its own directory.' +Assert-Matches $uninstall 'if not exist "%~dp0RegAsm\.exe" exit /b 2' 'UnInstall.bat must verify RegAsm.exe exists.' +Assert-Matches $uninstall 'if not exist "%~dp0SldWorksLookup\.dll" exit /b 3' 'UnInstall.bat must verify SldWorksLookup.dll exists.' +Assert-Matches $uninstall '"%~dp0RegAsm\.exe" "%~dp0SldWorksLookup\.dll" /u' 'UnInstall.bat must quote RegAsm and target paths.' +Assert-Matches $uninstall 'if errorlevel 1 exit /b %errorlevel%' 'UnInstall.bat must propagate RegAsm failures.' +Assert-Matches $uninstall 'exit /b 0' 'UnInstall.bat must return 0 only after success.' + +Assert-Matches $installer 'AI_REQUIRED_DOTNET_DISPLAY".*4\.7\.2' 'Installer display minimum .NET version must be 4.7.2.' +Assert-Matches $installer 'AI_REQUIRED_DOTNET_VERSION".*4\.7\.2' 'Installer launch condition minimum .NET version must be 4.7.2.' +Assert-NotMatches $installer 'File="exceptionless\.txt"' 'Installer must not include an explicit exceptionless.txt file row.' +Assert-NotMatches $installer 'File="[^"]+\.(pdb|xml)"' 'Installer must not include explicit PDB or XML file rows.' +Assert-Matches $installer 'ExcludePattern="[^"]*(^|[|])exceptionless\.txt([|]|")' 'Synchronized bin content must exclude exceptionless.txt.' +Assert-Matches $installer 'ExcludePattern="[^"]*(^|[|])\*\.pdb([|]|")' 'Synchronized bin content must exclude PDB files.' +Assert-Matches $installer 'ExcludePattern="[^"]*(^|[|])\*\.xml([|]|")' 'Synchronized bin content must exclude XML documentation files.' + +Assert-Matches $testsProject 'x64' 'Regression test runner must target x64 to avoid MSB3270.' + +[void](Assert-XmlFile 'SldWorksLookup\SldWorksLookup.csproj') +[void](Assert-XmlFile 'tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj') +[void](Assert-XmlFile 'Installer\SolidWorksLookup.aip') + +Write-Host 'Project configuration contract verified.'