Skip to main content

Create Plugin

Design Automation uses .bundle just like the Autodesk App Store, meaning you need to create a PackageContents.xml and a ZIP with the DLL (and other required files). For detailed information on how to create them, please visit Autodesk App Store Developer Center.

At this section we will create a basic plugin that update width and height parameter and save the resulting file. Also the supporting files (PackageContents.xml) and the folder structure to place them. Finally create a .ZIP file ready to upload to Design Automation.

In the root folder, create a bundles folder.

Prerequisites

  • 7zip: use to create the .ZIP with bundle files, please install from here. This tutorial assumes 7zip is installed on the default folder: C:\Program Files\7-Zip\7z.exe.

Additional prerequisites

For the next session you can use the pre-build plugin. Or if you decide to build it, you will need

  • Visual Studio: Visual Studio 2019 or newer is required, please visit this link.

  • AutoCAD, Inventor, Revit or 3ds Max: In order to develop, test and debug your Design Automation plugin: AutoCAD | Inventor | Revit | 3ds Max.


For the next step, choose the Engine, which is the Autodesk application where you plugin will run. You'll need the respective application installed in order to compile, debug and test locally.

Choose the engine

This step will help you create a basic AutoCAD plugin for Design Automation. For more information, please visit My First AutoCAD Plugin tutorial.

You may download the Bundle ZIP into the bundles/ (Node.js) or /designAutomationSample/wwwroot/bundles (.NET 6) folder and skip to Upload Plugin Bundle section.

Create a new project

  • Right-click on the solution, the Add >> New Project.
  • Select Windows Desktop, then Class Library and, finally, name it UpdateDWGParam.
  • Then right-click on the project, go to Manage NuGet Packages..., under Browser you can search for AutoCAD.NET and install AutoCAD.NET.Core (which also installs AutoCAD.NET.Model).
  • Then search and install Newtonsoft.Json (which is used to parse input data in JSON format).

Please select .NET Framework 4.8. If not listed, please install the Dev Pack.

As a result, the package.config should look like the following. This sample uses version 20, which should work on all available versions. You may adjust to a specific version.

The project should contain a Class1.cs class, let's rename the file to Commands.cs (for consistency).

Commands.cs
using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Newtonsoft.Json;
using System.IO;

[assembly: CommandClass(typeof(UpdateDWGParam.Commands))]
[assembly: ExtensionApplication(null)]

namespace UpdateDWGParam
{
public class Commands
{
[CommandMethod("UpdateParam", CommandFlags.Modal)]
public static void UpdateParam()
{
//Get active document of drawing with Dynamic block
var doc = Application.DocumentManager.MdiActiveDocument;
var db = doc.Database;

// read input parameters from JSON file
InputParams inputParams = JsonConvert.DeserializeObject<InputParams>(File.ReadAllText("params.json"));

using (Transaction t = db.TransactionManager.StartTransaction())
{
var bt = t.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;

foreach (ObjectId btrId in bt)
{
//get the blockDef and check if is anonymous
BlockTableRecord btr = (BlockTableRecord)t.GetObject(btrId, OpenMode.ForRead);
if (btr.IsDynamicBlock)
{
//get all anonymous blocks from this dynamic block
ObjectIdCollection anonymousIds = btr.GetAnonymousBlockIds();
ObjectIdCollection dynBlockRefs = new ObjectIdCollection();
foreach (ObjectId anonymousBtrId in anonymousIds)
{
//get the anonymous block
BlockTableRecord anonymousBtr = (BlockTableRecord)t.GetObject(anonymousBtrId, OpenMode.ForRead);
//and all references to this block
ObjectIdCollection blockRefIds = anonymousBtr.GetBlockReferenceIds(true, true);
foreach (ObjectId id in blockRefIds)
{
dynBlockRefs.Add(id);
}
}
if (dynBlockRefs.Count > 0)
{
//Get the first dynamic block reference, we have only one Dyanmic Block reference in Drawing
var dBref = t.GetObject(dynBlockRefs[0], OpenMode.ForWrite) as BlockReference;
UpdateDynamicProperties(dBref, inputParams);
}
}
}
t.Commit();
}
LogTrace("Saving file...");
db.SaveAs("outputFile.dwg", DwgVersion.Current);
}

/// <summary>
/// This updates the Dyanmic Blockreference with given Width and Height
/// The initial parameters of Dynamic Blockrefence, Width =20.00 and Height =40.00
/// </summary>
/// <param Editor="ed"></param>
/// <param BlockReference="br"></param>
/// <param String="name"></param>
private static void UpdateDynamicProperties(BlockReference br, InputParams inputParams)
{
// Only continue is we have a valid dynamic block
if (br != null && br.IsDynamicBlock)
{
// Get the dynamic block's property collection
DynamicBlockReferencePropertyCollection pc = br.DynamicBlockReferencePropertyCollection;
foreach (DynamicBlockReferenceProperty prop in pc)
{
switch (prop.PropertyName)
{
case "Width":
prop.Value = inputParams.Width;
break;
case "Height":
prop.Value = inputParams.Height;
break;
default:
break;
}
}
}
}

/// <summary>
/// This will appear on the Design Automation output
/// </summary>
private static void LogTrace(string format, params object[] args) { Application.DocumentManager.MdiActiveDocument.Editor.WriteMessage(format, args); }
}

public class InputParams
{
public double Width { get; set; }
public double Height { get; set; }
}
}

This is the main code that will run with AutoCAD. Copy the following content into Commands.cs. The class contains one custom AutoCAD command, UpdateParam, defined as a method with the same name. This command is called by Design Automation engine, as will be specified on the Activity (next step of this tutorial)

PackageContents.xml
<?xml version="1.0" encoding="utf-8" ?>
<ApplicationPackage SchemaVersion="1.0" Version="1.0" ProductCode="{F11EA57A-1E7E-4B6D-8E81-986B071E3E07}" Name="AutoCADDesignAutomation" Description="Sample Plugin for AutoCAD" Author="tutorials.autodesk.io>">
<CompanyDetails Name="Autodesk, Inc" Url="http://tutorials.autodesk.io" Email="forge.help@autodesk.com"/>
<Components>
<RuntimeRequirements OS="Win64" Platform="AutoCAD"/>
<ComponentEntry AppName="UpdateWindowParameters" ModuleName="./Contents/UpdateDWGParam.dll" AppDescription="AutoCAD .NET App to update parameters of Dynamic blockreference in AutoCAD Drawing" LoadOnCommandInvocation="True" LoadOnAutoCADStartup="True">
<Commands GroupName="FPDCommands">
<Command Global="UpdateParam" Local="UpdateParam"/>
</Commands>
</ComponentEntry>
</Components>
</ApplicationPackage>

Create a folder named UpdateDWGParam.bundle and, inside, a file named PackageContents.xml, then copy the following content to it. Learn more at the PackageContents.xml Format Reference. This file defines the new AutoCAD custom command UpdateParam that will be called when Design Automation executes.

Finally, create a subfolder named Contents and leave it empty. At this point, the project should look like:

Post-build event

For Node.js it is required to adjust the AppBundle ZIP output folder.

Now we need to ZIP the .bundle folder. Right-click on the project, select Properties, then open Build Events and copy the following into Post-build event command line field, as shown on the image below.

xcopy /Y /F "$(TargetDir)*.dll" "$(ProjectDir)UpdateDWGParam.bundle\Contents\"
del /F "$(ProjectDir)..\designAutomationSample\wwwroot\bundles\UpdateDWGParam.zip"
"C:\Program Files\7-Zip\7z.exe" a -tzip "$(ProjectDir)../designAutomationSample/wwwroot/bundles/UpdateDWGParam.zip" "$(ProjectDir)UpdateDWGParam.bundle\" -xr0!*.pdb

This will copy the DLL from /bin/debug/ into .bundle/Contents folder, then use 7zip to create a zip, then finally copy the ZIP into /bundles folders of the webapp.

Note how the Post-build event uses the project and folder names, so make sure you're using this names.

If you build the UpdateDWGParam project now you should see something like this on the Output window. Note the 2 folders and 3 files zipped. The zip file is created directly at the /wwwroot/bundles folder. This means you're doing great!

Upload Plugin Bundle

Now the ZIP bundle is ready, let's upload to Design Automation.

Inside routes/ folder create DesignAutomation.js file.

  • In this file we will write all the endpoints, we will add Utils class consisting of all the utility functions like creating design automation SDK instance, uploading file and few more helpfull functions which are used in this sample.
routes/DesignAutomation.js
const _path = require("path");
const _fs = require("fs");
const _url = require("url");
const express = require("express");
const http = require("https");
const formdata = require("form-data");
const bodyParser = require("body-parser");
const multer = require("multer");
const router = express.Router();
const { getClient } = require("./common/oauth");
const config = require("../config");
const dav3 = require("autodesk.forge.designautomation");
const ForgeAPI = require("forge-apis");

router.use(bodyParser.json());

// Middleware for obtaining a token for each request.
router.use(async (req, res, next) => {
req.oauth_client = await getClient(/*config.scopes.internal*/);
req.oauth_token = req.oauth_client.getCredentials();
next();
});

// Static instance of the DA API
let dav3Instance = null;

class Utils {
static async Instance() {
if (dav3Instance === null) {
// Here it is ok to not await since we awaited in the call router.use()
dav3Instance = new dav3.AutodeskForgeDesignAutomationClient(
config.client
);
let FetchRefresh = async (data) => {
// data is undefined in a fetch, but contains the old credentials in a refresh
let client = await getClient();
let credentials = client.getCredentials();
// The line below is for testing
//credentials.expires_in = 30; credentials.expires_at = new Date(Date.now() + credentials.expires_in * 1000);
return credentials;
};
dav3Instance.authManager.authentications["2-legged"].fetchToken =
FetchRefresh;
dav3Instance.authManager.authentications["2-legged"].refreshToken =
FetchRefresh;
}
return dav3Instance;
}

/// <summary>
/// Returns the directory where bindles are stored on the local machine.
/// </summary>
static get LocalBundlesFolder() {
return _path.resolve(_path.join(__dirname, "../", "bundles"));
}

/// <summary>
/// Prefix for AppBundles and Activities
/// </summary>
static get NickName() {
return config.credentials.client_id;
}

/// <summary>
/// Alias for the app (e.g. DEV, STG, PROD). This value may come from an environment variable
/// </summary>
static get Alias() {
return "dev";
}

/// <summary>
/// Search files in a folder and filter them.
/// </summary>
static async findFiles(dir, filter) {
return new Promise((fulfill, reject) => {
_fs.readdir(dir, (err, files) => {
if (err) return reject(err);
if (filter !== undefined && typeof filter === "string")
files = files.filter((file) => {
return _path.extname(file) === filter;
});
else if (filter !== undefined && typeof filter === "object")
files = files.filter((file) => {
return filter.test(file);
});
fulfill(files);
});
});
}

/// <summary>
/// Create a new DAv3 client/API with default settings
/// </summary>
static async dav3API(oauth2) {
// There is 2 alternatives to setup an API instance, providing the access_token directly
// let apiClient2 = new dav3.AutodeskForgeDesignAutomationClient(/*config.client*/);
// apiClient2.authManager.authentications['2-legged'].accessToken = oauth2.access_token;
//return (new dav3.AutodeskForgeDesignAutomationApi(apiClient));

// Or use the Auto-Refresh feature
let apiClient = await Utils.Instance();
return new dav3.AutodeskForgeDesignAutomationApi(apiClient);
}

/// <summary>
/// Helps identify the engine
/// </summary>
static EngineAttributes(engine) {
if (engine.includes("3dsMax"))
return {
commandLine:
'$(engine.path)\\3dsmaxbatch.exe -sceneFile "$(args[inputFile].path)" "$(settings[script].path)"',
extension: "max",
script:
"da = dotNetClass('Autodesk.Forge.Sample.DesignAutomation.Max.RuntimeExecute')\nda.ModifyWindowWidthHeight()\n",
};
if (engine.includes("AutoCAD"))
return {
commandLine:
'$(engine.path)\\accoreconsole.exe /i "$(args[inputFile].path)" /al "$(appbundles[{0}].path)" /s "$(settings[script].path)"',
extension: "dwg",
script: "UpdateParam\n",
};
if (engine.includes("Inventor"))
return {
commandLine:
'$(engine.path)\\InventorCoreConsole.exe /i "$(args[inputFile].path)" /al "$(appbundles[{0}].path)"',
extension: "ipt",
script: "",
};
if (engine.includes("Revit"))
return {
commandLine:
'$(engine.path)\\revitcoreconsole.exe /i "$(args[inputFile].path)" /al "$(appbundles[{0}].path)"',
extension: "rvt",
script: "",
};

throw new Error("Invalid engine");
}

static FormDataLength(form) {
return new Promise((fulfill, reject) => {
form.getLength((err, length) => {
if (err) return reject(err);
fulfill(length);
});
});
}

/// <summary>
/// Upload a file
/// </summary>
static uploadFormDataWithFile(filepath, endpoint, params = null) {
return new Promise(async (fulfill, reject) => {
const fileStream = _fs.createReadStream(filepath);

const form = new formdata();
if (params) {
const keys = Object.keys(params);
for (let i = 0; i < keys.length; i++)
form.append(keys[i], params[keys[i]]);
}
form.append("file", fileStream);

let headers = form.getHeaders();
headers["Cache-Control"] = "no-cache";
headers["Content-Length"] = await Utils.FormDataLength(form);

const urlinfo = _url.parse(endpoint);
const postReq = http.request(
{
host: urlinfo.host,
port: urlinfo.port || (urlinfo.protocol === "https:" ? 443 : 80),
path: urlinfo.pathname,
method: "POST",
headers: headers,
},
(response) => {
fulfill(response.statusCode);
},
(err) => {
reject(err);
}
);

form.pipe(postReq);
});
}
}
  • App Bundle

    Before creating activity, we need to define app bundle with plugin and selecting the appropriate engine. Copy & paste the following endpoints after the utils class.

/// <summary>
/// Names of app bundles on this project
/// </summary>
router.get("/appbundles", async (/*GetLocalBundles*/ req, res) => {
// this folder is placed under the public folder, which may expose the bundles
// but it was defined this way so it be published on most hosts easily
let bundles = await Utils.findFiles(Utils.LocalBundlesFolder, ".zip");
bundles = bundles.map((fn) => _path.basename(fn, ".zip"));
res.json(bundles);
});

/// <summary>
/// Return a list of available engines
/// </summary>
router.get(
"/aps/designautomation/engines",
async (/*GetAvailableEngines*/ req, res) => {
let that = this;
let Allengines = [];
let paginationToken = null;
try {
const api = await Utils.dav3API(req.oauth_token);
while (true) {
let engines = await api.getEngines({ page: paginationToken });
Allengines = Allengines.concat(engines.data);
if (engines.paginationToken == null) break;
paginationToken = engines.paginationToken;
}
res.json(Allengines.sort()); // return list of engines
} catch (ex) {
console.error(ex);
res.json([]);
}
}
);

/// <summary>
/// Define a new appbundle
/// </summary>
router.post(
"/aps/designautomation/appbundles",
async (/*CreateAppBundle*/ req, res) => {
const appBundleSpecs = req.body;

// basic input validation
const zipFileName = appBundleSpecs.zipFileName;
const engineName = appBundleSpecs.engine;

// standard name for this sample
const appBundleName = zipFileName + "AppBundle";

// check if ZIP with bundle is here
const packageZipPath = _path.join(
Utils.LocalBundlesFolder,
zipFileName + ".zip"
);

// get defined app bundles
const api = await Utils.dav3API(req.oauth_token);
let appBundles = null;
try {
appBundles = await api.getAppBundles();
} catch (ex) {
console.error(ex);
return res.status(500).json({
diagnostic: "Failed to get the Bundle list",
});
}
// check if app bundle is already define
let newAppVersion = null;
const qualifiedAppBundleId = `${Utils.NickName}.${appBundleName}+${Utils.Alias}`;
if (!appBundles.data.includes(qualifiedAppBundleId)) {
// create an appbundle (version 1)
// const appBundleSpec = {
// package: appBundleName,
// engine: engineName,
// id: appBundleName,
// description: `Description for ${appBundleName}`
// };
const appBundleSpec = dav3.AppBundle.constructFromObject({
package: appBundleName,
engine: engineName,
id: appBundleName,
description: `Description for ${appBundleName}`,
});
try {
newAppVersion = await api.createAppBundle(appBundleSpec);
} catch (ex) {
console.error(ex);
return res.status(500).json({
diagnostic: "Cannot create new app",
});
}

// create alias pointing to v1
const aliasSpec =
//dav3.Alias.constructFromObject({
{
id: Utils.Alias,
version: 1,
};
try {
const newAlias = await api.createAppBundleAlias(
appBundleName,
aliasSpec
);
} catch (ex) {
console.error(ex);
return res.status(500).json({
diagnostic: "Failed to create an alias",
});
}
} else {
// create new version
const appBundleSpec =
//dav3.AppBundle.constructFromObject({
{
engine: engineName,
description: appBundleName,
};
try {
newAppVersion = await api.createAppBundleVersion(
appBundleName,
appBundleSpec
);
} catch (ex) {
console.error(ex);
return res.status(500).json({
diagnostic: "Cannot create new version",
});
}

// update alias pointing to v+1
const aliasSpec =
//dav3.AliasPatch.constructFromObject({
{
version: newAppVersion.version,
};
try {
const newAlias = await api.modifyAppBundleAlias(
appBundleName,
Utils.Alias,
aliasSpec
);
} catch (ex) {
console.error(ex);
return res.status(500).json({
diagnostic: "Failed to create an alias",
});
}
}

// upload the zip with .bundle
try {
// curl https://bucketname.s3.amazonaws.com/
// -F key = apps/myApp/myfile.zip
// -F content-type = application/octet-stream
// -F policy = eyJleHBpcmF0aW9uIjoiMjAxOC0wNi0yMVQxMzo...(trimmed)
// -F x-amz-signature = 800e52d73579387757e1c1cd88762...(trimmed)
// -F x-amz-credential = AKIAIOSFODNN7EXAMPLE/20180621/us-west-2/s3/aws4_request/
// -F x-amz-algorithm = AWS4-HMAC-SHA256
// -F x-amz-date = 20180621T091656Z
// -F file=@E:myfile.zip
//
// The ‘file’ field must be at the end, all fields after ‘file’ will be ignored.
await Utils.uploadFormDataWithFile(
packageZipPath,
newAppVersion.uploadParameters.endpointURL,
newAppVersion.uploadParameters.formData
);
} catch (ex) {
console.error(ex);
return res.status(500).json({
diagnostic: "Failed to upload bundle on s3",
});
}

res.status(200).json({
appBundle: qualifiedAppBundleId,
version: newAppVersion.version,
});
}
);

module.exports = router;

Now let's "mount" the router to our server application by modifying the server.js:

server.js
const _path = require("path");
const express = require("express");
const cookieSession = require("cookie-session");
const config = require("./config");
if (!config.credentials.client_id || !config.credentials.client_secret)
return console.error(
"Missing APS_CLIENT_ID or APS_CLIENT_SECRET env variables."
);

let app = express();
app.use(express.static(_path.join(__dirname, "./wwwroot")));
app.use(
cookieSession({
name: "aps_session",
keys: ["aps_secure_key"],
maxAge: 60 * 60 * 1000, // 1 hour, same as the 2 legged lifespan token
})
);
app.use(
express.json({
limit: "50mb",
})
);

app.use("/api", require("./routes/DesignAutomation"));
app.set("port", process.env.PORT || 8080);

module.exports = app;

If you run the webapp now and click on Configure (top-right), you should see your AppBundle and a list of all available engines. Buttons do not work yet... let's move forward.