|
After a quite long C# Learning process I'm beginning with NET using VS2010, Autocad 2010
I tried mixing up two code snippets found in the net to achive the result of adding a tab-ribbon-button and have one button draw a line.
everything works well till I draw that line where it crashes.
I tried using two different AddLine() method overloads, both in MyLines class, but with the same end.
in the first AddLine() method it crashes just after the
codelineCode:MessageBox.Show("Open the Block table record Model space for write");
in the 2nd AddLine(Point3d startPoint, Point3d endPoint) method it crashes just after thecodelineCode:MessageBox.Show("open BlockTabelRecord for write");
I add my VS2010 solution and ask for possible help. where you can also find (but I feel you already knew that) acad.err file in \bin\Debug folder
Possibly I'd also ask for some explanation as for differences I can see in the two methods while managing databases and transactions
thank you
I am unable to download your attachment at this time (I've asked others to look into that); could you simply post your code (entire namespace[s], and class[es] you're using)?
Cheers
"How we think determines what we do, and what we do determines what we get."
Sincpac C3D ~ Autodesk Exchange Apps
Computer Specs:
Dell Precision 3660, Core i9-12900K 5.2GHz, 64GB DDR5 RAM, PCIe 4.0 M.2 SSD (RAID 0), 16GB NVIDIA RTX A4000
this is Command.cs
and this is MyLines.csCode:using System; using System.Windows.Media.Imaging; using System.Reflection; using System.Windows; using Autodesk.Windows; using Autodesk.AutoCAD.Runtime; using Autodesk.AutoCAD.ApplicationServices; using acApp = Autodesk.AutoCAD.ApplicationServices.Application; namespace MySplitButton { public class Commands { BitmapImage getBitmap(string fileName) { BitmapImage bmp = new BitmapImage(); // BitmapImage.UriSource must be in a BeginInit/EndInit block. bmp.BeginInit(); bmp.UriSource = new Uri(string.Format("pack://application:,,,/{0};component/{1}", Assembly.GetExecutingAssembly().GetName().Name, fileName)); bmp.EndInit(); return bmp; } [CommandMethod("RibbonSplitButton")] public void RibbonSplitButton() { // Autodesk.Windows.RibbonControl ribbonControl = Autodesk.Windows.ComponentManager.Ribbon; RibbonControl ribbonControl = ComponentManager.Ribbon; // create Ribbon tab RibbonTab Tab = new RibbonTab(); Tab.Title = "Test Ribbon"; Tab.Id = "TESTRIBBON_TAB_ID"; ribbonControl.Tabs.Add(Tab); // create Ribbon panel // Autodesk.Windows.RibbonPanelSource srcPanel = new RibbonPanelSource(); RibbonPanelSource srcPanel = new RibbonPanelSource(); srcPanel.Title = "Panel1"; RibbonPanel Panel = new RibbonPanel(); Panel.Source = srcPanel; Tab.Panels.Add(Panel); // create the buttons listed in the split button // Autodesk.Windows.RibbonButton button1 = new RibbonButton(); RibbonButton button1 = new RibbonButton(); button1.Text = "Button1"; button1.ShowText = true; button1.ShowImage = true; button1.LargeImage = getBitmap("a_large.png"); button1.Image = getBitmap("a_small.png"); button1.CommandHandler = new MyCmdHandler(); // Autodesk.Windows.RibbonButton button2 = new RibbonButton(); RibbonButton button2 = new RibbonButton(); button2.Text = "Button2"; button2.ShowText = true; button2.ShowImage = true; button2.LargeImage = getBitmap("b_large.png"); button2.Image = getBitmap("b_small.png"); button2.CommandHandler = new MyCmdHandler(); // create split button RibbonSplitButton ribSplitButton = new RibbonSplitButton(); ribSplitButton.Text = "RibbonSplitButton"; //Required not to crash AutoCAD when using cmd locator ribSplitButton.ShowText = true; ribSplitButton.Items.Add(button1); ribSplitButton.Items.Add(button2); // If you want your button to look like this instead, then add the below lines //ribSplitButton.IsSplit = true; //ribSplitButton.Size = RibbonItemSize.Large; //ribSplitButton.IsSynchronizedWithCurrentItem = true; srcPanel.Items.Add(ribSplitButton); Tab.IsActive = true; } public class MyCmdHandler : System.Windows.Input.ICommand { public bool CanExecute(object parameter) { return true; } public event EventHandler CanExecuteChanged; public void Execute(object parameter) { Document doc = acApp.DocumentManager.MdiActiveDocument; if (parameter is RibbonButton) { RibbonButton button = parameter as RibbonButton; doc.Editor.WriteMessage("\nRibbonButton Executed: " + button.Text + "\n"); switch (button.Text) { case "Button1": MyLines myLine = new MyLines(); myLine.AddLine(); break; case "Button2": MyCircles myCircle = new MyCircles(); myCircle.DrawCircle(); break; } } } } } }
As compared to the solution I posted I made some little changes, both in calling AddLine() at "Button1" clicking and adding "MyCircle" class (which follows in the next post) to try and have a circle drawn at clicking "Button2", also. But still with no results: I launch Autocad, netload "MySplitButton.dll" and enter "RibbonSplitButton" commandCode:using System.Windows.Forms; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.Geometry; using Autodesk.AutoCAD.DatabaseServices; using acApp = Autodesk.AutoCAD.ApplicationServices.Application; namespace MySplitButton { class MyLines { //public void myLine() //{ // //AddLine(); / for calling the first overload method // Point3d startPoint = new Point3d(0, 0, 0); // Point3d endPoint = new Point3d(500, 500, 0); // AddLine(startPoint, endPoint); //} public void AddLine() { MessageBox.Show("I'm in AddLine()"); // Get the current document and database Document acDoc = acApp.DocumentManager.MdiActiveDocument; Database acCurDb = acDoc.Database; // Start a transaction using (Transaction acTrans = acCurDb.TransactionManager.StartTransaction()) { MessageBox.Show("Open the Block table for read"); // Open the Block table for read BlockTable acBlkTbl; acBlkTbl = acTrans.GetObject(acCurDb.BlockTableId, OpenMode.ForRead) as BlockTable; MessageBox.Show("Open the Block table record Model space for write"); // Open the Block table record Model space for write BlockTableRecord acBlkTblRec; acBlkTblRec = acTrans.GetObject(acBlkTbl[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord; MessageBox.Show("Create a line that starts at 5,5 and ends at 12,3"); // Create a line that starts at 5,5 and ends at 12,3 using (Line acLine = new Line(new Point3d(5, 5, 0), new Point3d(12, 3, 0))) { MessageBox.Show("Add the new object to the block table record and the transaction"); // Add the new object to the block table record and the transaction acBlkTblRec.AppendEntity(acLine); MessageBox.Show("acTrans.AddNewlyCreatedDBObject(acLine, true)"); acTrans.AddNewlyCreatedDBObject(acLine, true); } // Save the new object to the database acTrans.Commit(); } } // Method Overload for AddLine() which accepts two Point3d arguments public void AddLine(Point3d startPoint, Point3d endPoint) { Database db = HostApplicationServices.WorkingDatabase; MessageBox.Show("start the transaction"); // start the transaction using (Transaction tr = db.TransactionManager.StartOpenCloseTransaction()) { MessageBox.Show("open the block table for read"); // open the block table for read BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead); MessageBox.Show("conditionally open Paperspace or Modelspace/Active PViewport"); // conditionally open Paperspace or Modelspace/Active PViewport ObjectId spaceId = (1 == GetVarInt("CVPORT") ? SymbolUtilityServices.GetBlockPaperSpaceId(db) : SymbolUtilityServices.GetBlockModelSpaceId(db)); MessageBox.Show("open BlockTabelRecord for write"); BlockTableRecord btr = (BlockTableRecord)tr.GetObject(spaceId, OpenMode.ForWrite); MessageBox.Show("create a line that uses the supplied startPoint, and endPoint"); // create a line that uses the supplied startPoint, and endPoint Line oLine = new Line(startPoint, endPoint); MessageBox.Show("add the new object to the block table record and the transaction"); // add the new object to the block table record and the transaction btr.AppendEntity(oLine); tr.AddNewlyCreatedDBObject(oLine, true); MessageBox.Show("save the new object to the database"); // save the new object to the database tr.Commit(); } } private int GetVarInt(string sysVar) { return System.Convert.ToInt32(acApp.GetSystemVariable(sysVar)); } } }
after which a new "Test Ribbon" tab appears along with a "Panel1" panel with a split Button. Whether I click Button1 or Button2 there everything crashes..
and this is MyCircles.cs
if you need something else just ask for itCode:using System.Windows.Forms; //using System.Collections.Generic; //using System.Linq; //using System.Text; //using Autodesk.Windows; //using Autodesk.AutoCAD.Runtime; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.Geometry; //using Autodesk.AutoCAD.Runtime; using Autodesk.AutoCAD.DatabaseServices; using acApp = Autodesk.AutoCAD.ApplicationServices.Application; namespace MySplitButton { class MyCircles { //public void MyCircle() //{ //} //[CommandMethod("CreateCircle")] public void DrawCircle() { //Step one - Start a transaction using (Transaction trans = StartActiveDatabaseTransaction()) { //Step two - Create the new object Circle newCircle = CreateCircle(); //Step three - Open the symbol table BlockTableRecord modelSpace = OpenActiveModelSpaceForWrite(trans); //Step four - Append the object to the symbol table modelSpace.AppendEntity(newCircle); //Step five - Inform the transactionmanager of the new object trans.TransactionManager.AddNewlyCreatedDBObject(newCircle, true); //Step six - Commit the transaction trans.Commit(); } } private Transaction StartActiveDatabaseTransaction() { return HostApplicationServices.WorkingDatabase.TransactionManager.StartTransaction(); } private Circle CreateCircle() { Point3d centerPoint = new Point3d(0, 0, 0); Vector3d normal = new Vector3d(0, 0, 1); double radius = 4.5; return new Circle(centerPoint, normal, radius); } private BlockTableRecord OpenActiveModelSpaceForWrite(Transaction trans) { return (BlockTableRecord)trans.GetObject(GetActiveModelSpaceId(), OpenMode.ForWrite); } private ObjectId GetActiveModelSpaceId() { using (Transaction trans = StartActiveDatabaseTransaction()) { BlockTable bt = (BlockTable)trans.GetObject(HostApplicationServices.WorkingDatabase.BlockTableId, OpenMode.ForRead); return (ObjectId)bt[BlockTableRecord.ModelSpace]; } } //Page 16 //Adding non graphic objects //Variations of the six steps public static ObjectId CreateLayer(string layerName, Database targetDatabase) { //Step one - Start a transaction using (Transaction trans = targetDatabase.TransactionManager.StartTransaction()) { ObjectId layerTableId = targetDatabase.LayerTableId; //Step three - Open the symbol table //This table has only been opened for reading LayerTable layers = (LayerTable)trans.GetObject(layerTableId, OpenMode.ForRead); if (layers.Has(layerName)) { return layers[layerName]; } else { //Step two - Create the new object LayerTableRecord newLayer = new LayerTableRecord(); newLayer.Name = layerName; //Step three - Open the symbol table for writing layers.UpgradeOpen(); //Step four - Append the object to the symbol table layers.Add(newLayer); //Step five - Inform the transactionmanager of the new object trans.TransactionManager.AddNewlyCreatedDBObject(newLayer, true); //Step six - Commit the transaction trans.Commit(); return newLayer.ObjectId; } } } //[CommandMethod("CreateLayer")] public void CreateLayer() { string newLayerName = "AU2005"; CreateLayer(newLayerName, HostApplicationServices.WorkingDatabase); } } }
thank you for your time
The issue lies within your MyCmdHandler Class implementation.
The command handler class, derived from ICommand, executes the command assigned to a given RibbonButton via CommandHandler Property using SendStringToExecute() Method - not by calling the MyLines.AddLine() Method directly as you've defined it.
Here's a slight adaptation to the code you posted above, which addresses this for both "Button1" (by calling the new "AddLine" CommandMethod), and "Button2" (by calling the new "AddCircle" CommandMethod) respectively:
** I've commented out your bitmap calls as I do not have access to your earlier attachment, and simply wanted to avoid Exceptions, etc.Code:// using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Geometry; using Autodesk.AutoCAD.Runtime; using Autodesk.Windows; using System; using System.Reflection; using System.Windows; using System.Windows.Media.Imaging; using acApp = Autodesk.AutoCAD.ApplicationServices.Application; [assembly: CommandClass(typeof(MySplitButton2.Commands))] namespace MySplitButton2 { public class Commands { [CommandMethod("AddCircle")] public void AddCircleCmd() { AddCircle(); } [CommandMethod("AddLine")] public void AddLineCmd() { AddLine(); } [CommandMethod("RibbonSplitButton")] public void RibbonSplitButton() { string ribbonTabId = "TESTRIBBON_TAB_ID"; string ribbonTabTitle = "Test Ribbon"; RibbonControl ribbonControl = ComponentManager.Ribbon; // check if the test ribbon tab already exists RibbonTab ribbonTab = ribbonControl.FindTab(ribbonTabId); // if so.... if (ribbonTab != null) { // ... set as visible, and active (in that order) ribbonTab.IsVisible = true; ribbonTab.IsActive = true; return; } // ... otherwise, create the ribbon tab, and set as active ribbonTab = new RibbonTab(); ribbonTab.Id = ribbonTabId; ribbonTab.Title = ribbonTabTitle; ribbonControl.Tabs.Add(ribbonTab); // create Ribbon panel RibbonPanelSource ribbonPanelSource = new RibbonPanelSource(); ribbonPanelSource.Title = "Panel1"; RibbonPanel Panel = new RibbonPanel(); Panel.Source = ribbonPanelSource; ribbonTab.Panels.Add(Panel); // create the buttons listed in the split button RibbonButton button1 = new RibbonButton(); button1.Text = "Button1"; button1.ShowText = true; //button1.ShowImage = true; //button1.LargeImage = GetBitmap("a_large.png"); //button1.Image = GetBitmap("a_small.png"); button1.CommandHandler = new AdskCommandHandler(); RibbonButton button2 = new RibbonButton(); button2.Text = "Button2"; button2.ShowText = true; //button2.ShowImage = true; //button2.LargeImage = GetBitmap("b_large.png"); //button2.Image = GetBitmap("b_small.png"); button2.CommandHandler = new AdskCommandHandler(); // create split button RibbonSplitButton ribSplitButton = new RibbonSplitButton(); ribSplitButton.Text = "RibbonSplitButton"; //Required not to crash AutoCAD when using cmd locator ribSplitButton.ShowText = true; ribSplitButton.Items.Add(button1); ribSplitButton.Items.Add(button2); ribbonPanelSource.Items.Add(ribSplitButton); ribbonTab.IsActive = true; } private void AddCircle() { Database db = HostApplicationServices.WorkingDatabase; // start the transaction using (Transaction tr = db.TransactionManager.StartOpenCloseTransaction()) { // open the block table for read BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead); // conditionally open Paperspace or Modelspace/Active PViewport ObjectId spaceId = (1 == GetVarInt("CVPORT") ? SymbolUtilityServices.GetBlockPaperSpaceId(db) : SymbolUtilityServices.GetBlockModelSpaceId(db)); BlockTableRecord btr = (BlockTableRecord)tr.GetObject(spaceId, OpenMode.ForWrite); // create a line that uses the pre-defined center, normal, and radius Circle oCircle = new Circle(new Point3d(0, 0, 0), new Vector3d(0, 0, 1), 4.5); // add the new object to the block table record and the transaction btr.AppendEntity(oCircle); tr.AddNewlyCreatedDBObject(oCircle, true); // save the new object to the database tr.Commit(); } } private void AddLine() { Database db = HostApplicationServices.WorkingDatabase; // start the transaction using (Transaction tr = db.TransactionManager.StartOpenCloseTransaction()) { // open the block table for read BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead); // conditionally open Paperspace or Modelspace/Active PViewport ObjectId spaceId = (1 == GetVarInt("CVPORT") ? SymbolUtilityServices.GetBlockPaperSpaceId(db) : SymbolUtilityServices.GetBlockModelSpaceId(db)); BlockTableRecord btr = (BlockTableRecord)tr.GetObject(spaceId, OpenMode.ForWrite); // create a line that uses the pre-defined startPoint, and endPoint Line oLine = new Line(new Point3d(5, 5, 0), new Point3d(12, 3, 0)); // add the new object to the block table record and the transaction btr.AppendEntity(oLine); tr.AddNewlyCreatedDBObject(oLine, true); // save the new object to the database tr.Commit(); } } //BitmapImage GetBitmap(string fileName) //{ // BitmapImage bmp = new BitmapImage(); // // BitmapImage.UriSource must be in a BeginInit/EndInit block. // bmp.BeginInit(); // bmp.UriSource = new Uri(string.Format("pack://application:,,,/{0};component/{1}", // Assembly.GetExecutingAssembly().GetName().Name, // fileName)); // bmp.EndInit(); // return bmp; //} private int GetVarInt(string sysVar) { return System.Convert.ToInt32(acApp.GetSystemVariable(sysVar)); } } public class AdskCommandHandler : System.Windows.Input.ICommand { private DocumentCollection acDocs = acApp.DocumentManager; public bool CanExecute(object parameter) { return true; } public event EventHandler CanExecuteChanged; public void Execute(object parameter) { Document doc = acDocs.MdiActiveDocument; if (parameter is RibbonButton) { RibbonButton button = parameter as RibbonButton; doc.Editor.WriteMessage("\nRibbonButton Executed: " + button.Text + "\n"); switch (button.Text) { case "Button1": //MyLines myLine = new MyLines(); //myLine.AddLine(); acDocs.MdiActiveDocument.SendStringToExecute("AddLine ", true, false, true); break; case "Button2": //MyCircles myCircle = new MyCircles(); //myCircle.DrawCircle(); acDocs.MdiActiveDocument.SendStringToExecute("AddCircle ", true, false, true); break; } } } } }
Cheers
Last edited by BlackBox; 2015-01-05 at 09:21 PM.
"How we think determines what we do, and what we do determines what we get."
Sincpac C3D ~ Autodesk Exchange Apps
Computer Specs:
Dell Precision 3660, Core i9-12900K 5.2GHz, 64GB DDR5 RAM, PCIe 4.0 M.2 SSD (RAID 0), 16GB NVIDIA RTX A4000
Thank you BlackBox, it worked!
I dived into your code making some modifications as:
1) adding an "else" after "if (ribbonTab != null)" statements block
2) commenting outCode:// if so.... if (ribbonTab != null) { // ... set as visible, and active (in that order) ribbonTab.IsVisible = true; ribbonTab.IsActive = true; return; } else { // ... otherwise, create the ribbon tab, and set as active ribbonTab = new RibbonTab(); ribbonTab.Id = ribbonTabId; ribbonTab.Title = ribbonTabTitle; ribbonControl.Tabs.Add(ribbonTab); }could you tell me the following:Code:[assembly: CommandClass(typeof(MySplitButton2.Commands))]
- why moving AdskCommandHandler class outside Command class (where previously resided MyCmdHandler class)?
- what drawbacks in using SendCommand or RunCommand methods? Is one of these suitable for making these buttons open new forms where users can click and run other methods?
finally I'd like to debug as in VBA but I can't. I place BreakPoints and click StartDebugging buton, but the code flows with no interruptions. Am I missing some VS configuration details?
thank you again
You're very welcome, RICVBA.
You're certainly able to change what you like syntactically (to make it easier for you to 'read' and maintain).
For the purposes of clarity, there is no effective difference in the code being evaluated, as the logic is the same... More specifically, by testing that ribbonTab != null, and then ending that if statements 'then' expression with return; you preclude any of the code that follows from being evaluated. By adding brackets around both the 'then' expression, and the 'else' expression, you could remove the call to return; in the 'then' expression altogether and you're left with the same functional equivalent, and simply more typing to do in order to implement the same code logic.
The CommandClass attribute tells AutoCAD precisely which Class defines your CommandMethod Methods... It's not required, but *can* be beneficial depending on the scale, and scope of your plug-in, and so I include such were applicable.
It really has no affect either way, for such a simple plug-in... The nesting level only becomes a factor when dealing with permission keywords, and this Class having permission to instantiate that Type, while not being exposed to the other Class, and so on.
There are myriad ways to call Commands in .NET, and each has their purpose... Some run Synchronously, others Asynchronously, some accept parameters and pause tokens, others require a specific string.
To give you a better idea of what does what, and where we are now, consider these two different articles by Kean on calling Commands in .NET API:
August 30, 2006 - Calling AutoCAD commands from .NET
March 31, 2014 - AutoCAD 2015: calling commands
You can debug your project in AutoCAD either using the AutoCAD .NET Wizard project template (scroll down) + Visual Studio Express, or with full Visual Studio.
The former simply includes some appropriate XmlNodes, and XmlAttribute values to do so that are not exposed to VS Express IDE, whereas full VS allows you to configure same in Project Options.
If you're still having trouble getting debug to work, please let me know exactly what you're using to code for 2010, and I or someone smarter than I will try to be of some help there.
Cheers
"How we think determines what we do, and what we do determines what we get."
Sincpac C3D ~ Autodesk Exchange Apps
Computer Specs:
Dell Precision 3660, Core i9-12900K 5.2GHz, 64GB DDR5 RAM, PCIe 4.0 M.2 SSD (RAID 0), 16GB NVIDIA RTX A4000
thanks for your answers
It seems my "quite long" C# learning process didn't have much effect. And I also haven't paid much attention to your code thus missing that "return" statement.
as for the other more deep subjects (assembly, nesting level, permission keywords, exposition to other Class, and so on) I'd need an even more "quite long" learning process. which most probably would extend beyond my life span. so let's take it apart for the moment being!
thanks for Kean's articles. one (the older) was what I started from for asking you about the best method. but, once again, I got it that I'd need two lives to only scratch the surface of it. and in the meanwhile Autocad would reach issue 2060 and Net 20.4. thus making all that scratching unuseful for me.
so I go on with VBA and in parallel trying to mess up with some NET plugins
for this latter subject I'm using both VS Express 2010 and VS 2010 Ultimate, on two different PCs
As for now I'm on VS 2010 Ultimate and I still can't get started with "VBA" debugging having found nothing useful in "Project" tab (where there's no "Option" choice by the way)
You're welcome.
I've been a student of .NET for +/- 2 years, and still feel like just that - a student.
There is so much to learn, I take it on as needed, or where some topic really captures my interest, but it is quite difficult to jump right into the deep end, as it were. If you're already adept at another API, it's really easy to go back to that, rather than push through. There's another great series of articles that discusses 'the right tool for the job' in terms of APIs, and it's true - I still use VLIDE for 'scripting' tasks during a project, and end up using .NET for the apps I want to be long term projects, particularly where LISP is incapable, and coding a new LispFunction Method is too timely, or not needed.
It gets easier, the more you code, the more you read, and the more you code again. It does. You may not realize, or even be able to use what you're picking up conceptually now, for a year... But nothing beats that 'Booyah!' moment (RIP Stuart Scott), when you become aware that you do know how to do 'that thing' you were trying to do! Keep at it, and I'm certain you'll do well.
As for debugging, not sure how you're moving your current code between PCs (I used a Google Drive folder that is associated with Tortoise SVN for sub-versioning; pretty sweet actually), but VS Express is dependent on the Project\PropertyGroup\StartProgram XmlNode of your *.csproj.user file (configured via AutoCAD .NET Wizard), and cannot be modified from VS IDE, only manually in Notepad++, etc. once initial values have been set. VS Ultimate on the other hand can edit these settings within the IDE, simply right click your project, select Properties, Debug tab, and specify the appropriate value for 'Start external program', etc.
With that set correctly, you simply hit the 'play' button in toolbar, or right click your project, and select debug from the context menu... You'll have to NETLOAD your assembly still, into the debug session that starts (or instead 'build' to your app's Autoloader .bundle via relative path?), but then, your breakpoints will be observed.
Important to note, especially if you're using same project with both applications, is that both the starting application, and the location of the ObjectARX assembly references need to reside in the same location on both PCs (not co-located), in order to preclude the need to modify each-and-every-single-time you switch to the other machine.
Cheers
"How we think determines what we do, and what we do determines what we get."
Sincpac C3D ~ Autodesk Exchange Apps
Computer Specs:
Dell Precision 3660, Core i9-12900K 5.2GHz, 64GB DDR5 RAM, PCIe 4.0 M.2 SSD (RAID 0), 16GB NVIDIA RTX A4000