Skip to main content

Civil 3D

This guide explains how to integrate Autodesk Civil 3D with a VIKTOR app. Your app calls the Civil 3D COM (ActiveX) API directly, so you can read the surfaces, alignments and COGO points in a drawing, add to them, and draw with the full AutoCAD drafting surface, all from your app code.

note

vkt.civil3d.connect() and vkt.civil3d.attach() are a BETA feature and require viktor >= 14.36.0.

vkt.civil3d.connect() starts a Civil 3D instance on the worker for the session. vkt.civil3d.attach() binds the session to the instance the user already has open instead of starting a new one; see Attaching to a running instance.

The worker

A worker is a program that connects the VIKTOR platform to third-party software running outside the platform. For Civil 3D it belongs on the machine where you use Civil 3D, as a personal worker: Civil 3D must be installed where the worker is, and a session drives the real desktop application. Your VIKTOR app handles the communication between the web app and the worker.

Here is what happens during the integration:

  1. The user performs an action that requires a Civil 3D analysis.
  2. Your app opens a session, and each ActiveX call it makes is sent to the worker as a task.
  3. The worker executes that one call against the live Civil 3D instance and returns its result.
  4. Your app uses the returned values to build its views, or draws the next piece of geometry.
  5. (Optional) VIKTOR UI displays the result.

Unlike the integrations that send a file or a script to the worker, there is no script to write and ship: all of your code stays in your app, and the worker machine needs no Python environment.

Civil 3D cannot run headless

The worker drives the real desktop application, so the machine it runs on needs a logged-in, interactive Windows session (a service running in Session 0 cannot start Civil 3D). The licence must not prompt on startup either: nobody is there to answer an Autodesk sign-in or activation dialog, so the app would only see a timeout. Activate the licence once by launching Civil 3D by hand on that machine, as the same user the worker runs as.

Calling the API directly from your app

Open a session with vkt.civil3d.connect() and navigate the Civil 3D object model from the c3d object it gives you:

app.py
import viktor as vkt


class Controller(vkt.Controller):
parametrization = Parametrization

def write_to_civil3d(self, params, **kwargs):
with vkt.civil3d.connect(timeout=180) as c3d:
document = c3d.ActiveDocument

point = document.Points.Add([params.easting, params.northing, params.level])
number = point.Number() # property read: call syntax (note the parentheses)
point.Description = "SET-OUT" # property write: plain assignment

document.SaveAs("setting-out.dwg") # relative filename, not an absolute path

Please consider the following:

  • A property is read with parentheses and written without them. point.Number() gives you the value; point.Description = "SET-OUT" sets it. This is the convention that catches people out most often, so it is worth checking first when a call behaves unexpectedly. Reading a property without the parentheses raises a TypeError as soon as you use it as a value, before any round trip, with a hint naming the call you meant.
  • Objects that calls return are session-scoped handles. Use them as the start of further calls and as arguments, exactly like the COM objects they stand for. They are released when the session closes, so read the values you need before the with block ends.
  • Collections are read one item at a time, and they are 0-based. Use Count() and Item(i); Item also takes a name, so document.Surfaces.Item("EG") is a lookup rather than a scan. A Python for loop over a collection raises: there is no iteration protocol on a handle.
  • COGO points are Easting, Northing, Elevation. Points.Add([1000.0, 2000.0, 3.5]) in that order, read back with Easting(), Northing() and Elevation(). Reversing the first two misplaces every point without any error, so it is worth asserting the round trip in a test.
  • Navigate down from c3d. The back-references Application, Document and Parent are refused, so keep your own reference to c3d.ActiveDocument instead of walking back up.
  • Every call is a round trip to the worker; navigating attributes to reach it is free. c3d.ActiveDocument.Points.Count() is one round trip, not three. Bound loops that call or read properties per item, and prefer the calls that work in bulk over a loop that touches every object of a large drawing.
  • Filenames are confined to the session's working directory. Give SaveAs, ImportPoints, Surfaces.ImportXML and the other file members a relative filename rather than an absolute path. In this release a drawing cannot be returned from the session to your app, so read the values you need through COM while the session is open.
  • Some members are refused by the worker and raise vkt.ExecutionError: those that execute arbitrary code (SendCommand, Run, GetInterfaceObject), those that wait for input at the command line, those that change settings outliving the session (Preferences, SetVariable), and Save, Close and Quit. Save with SaveAs and a relative filename instead. Civil 3D's Aecc objects inherit the AutoCAD surface, so the same refusals hold on them.
  • The enumerations live on vkt.civil3d, covering the AutoCAD Ac* enumerations as well as Civil 3D's Aec* and Aecc* ones (AeccDrawingUnitType, AeccProfilePVICurveType, AcMeasurementUnits, and so on): vkt.civil3d.AeccDrawingUnitType.aeccDrawingUnitMeters. Their member names mirror the product API exactly, so the enum reads like the reference and beats a magic number. from viktor.external.civil3d import AeccDrawingUnitType gives you the same object under a shorter name. Note that they are not on the top-level vkt namespace, so vkt.AeccDrawingUnitType raises AttributeError.
  • Creation members vary between Civil 3D releases. The COM object model is not identical from one release to the next, and the corridor domain is largely read-only. Where a member creates something, check it against the release you are automating rather than assuming the signature carries over.

The COM members you call on c3d mirror the Civil 3D object model (the AeccXUi* type libraries) plus the AutoCAD ActiveX model it extends, and are not listed in the SDK reference. Look them up in the Civil 3D Developer's Guide ("COM API" chapters), and in the AutoCAD ActiveX Reference Guide for the drafting surface.

The three domain roots

Civil 3D splits its API over several application objects, and c3d is one of them: it is the Land root, whose ActiveDocument carries the surfaces, COGO points, alignments, sites and styles, plus the whole AutoCAD drafting surface. The other two domains are separate roots, reached as properties of c3d:

app.py
with vkt.civil3d.connect(timeout=180) as c3d:
surfaces = c3d.ActiveDocument.Surfaces.Count() # Land: the connection root
networks = c3d.pipe.ActiveDocument.PipeNetworks.Count() # pipe networks
corridors = c3d.roadway.ActiveDocument.Corridors.Count() # corridors and assemblies
  • c3d is the Land root. There is no c3d.land, and your app never calls GetInterfaceObject to reach a domain: the worker owns that, and the member is refused.
  • c3d.pipe is the pipe-network root. Its ActiveDocument is the same drawing seen through the pipe domain, and holds PipeNetworks.
  • c3d.roadway is the corridor root, whose ActiveDocument carries Corridors, Assemblies and Subassemblies. This domain is largely read-only in Civil 3D's COM API, so plan on reading a corridor rather than building one.

Each root is resolved on the worker the first time you touch it, which costs one round trip, and is then cached for the rest of the session. The cache does not outlive the session: a root taken from a closed session raises, like any other handle.

Attaching to a running instance

vkt.civil3d.attach() opens the same kind of session against the Civil 3D that is already running on the worker machine, rather than starting a fresh instance. The app then reads and writes the drawing the user has on screen, and what it draws appears in their window. This is what you want when the point is to act on the drawing in front of the user, which in practice means a personal worker on their own machine.

app.py
def read_from_civil3d(self, params, **kwargs):
try:
with vkt.civil3d.attach(timeout=180) as c3d:
surface = c3d.ActiveDocument.Surfaces.Item(params.surface_name)
return surface.Statistics.MinElevation()
except vkt.WorkerSessionAttachError as err:
if err.reason == "error_attach_no_instance":
raise vkt.UserError(
"No Civil 3D instance was found on your worker. Open your drawing in Civil 3D on "
"the machine running your personal worker, then try again. If plain AutoCAD is "
"open there, close it first."
) from err
if err.reason == "error_attach_wrong_product":
raise vkt.UserError(
f"Your Civil 3D runs a different units profile than your worker expects: {err}"
) from err
raise vkt.UserError(f"Could not attach to Civil 3D ({err.reason or 'unknown reason'}).") from err

The two are strictly separate:

  • attach() fails when nothing attachable is running on the worker machine; it never starts Civil 3D for you.
  • connect() always starts a fresh instance. If any AutoCAD-family application is already open on the worker machine the session is refused — close it there, or use attach().
  • There is no automatic fallback in either direction. If you want one, write it yourself by catching vkt.WorkerSessionAttachError and calling connect().

What changes once the session is attached:

  • The drawing is not empty. Do not assume the surfaces and point groups you expect are there, that layer names are free, or that the coordinate system is at its defaults.
  • Closing the session detaches without exiting Civil 3D. The instance and the drawing stay as they were, including everything your app wrote.
  • Nothing rolls back. Your app's changes are real edits in the user's drawing, and undoing them is their call. Write onto layers with a recognisable prefix so the result is easy to isolate.
  • The user's file on disk is still never overwritten: Save is refused, and SaveAs writes into the session's working directory.
Trigger it from an action button, not a download button

An attached session produces edits in the drawing already open on the user's screen — they watch the points or geometry appear, and saving the file is theirs to do. A DownloadButton promises a file that is never coming, and a save dialog is not what anyone expects after editing a model in front of them. Use a vkt.ActionButton when your app only writes to the drawing, and a vkt.SetParamsButton when it also feeds values back into the parametrization. Keep DownloadButton for output your app builds itself, such as a report or a CSV of values read through COM.

vkt.errors.WorkerSessionAttachError carries a reason; the reference page lists the full set of eight. The ones specific to a Civil 3D session:

reasonMeaning
"error_attach_no_instance"nothing attachable is running: either no instance at all, or the one that is running is plain AutoCAD, which must be closed first
"error_attach_wrong_product"the running instance uses a different units profile than the worker's units setting; the message names both profiles
"error_attach_refused"an instance was found but would not accept the connection, typically because it is busy or waiting on a dialog

The rest come from the worker connection itself rather than Civil 3D — for example "no_worker_online" (no worker is connected) and "worker_kind_not_allowed" (this worker kind is disabled for the organization) are usually the first ones a user hits. Handle an unrecognised reason as a generic attach failure. Note that WorkerSessionAttachError is a subclass of vkt.WorkerSessionError, so catch it first if you handle both.

Add integration to app config

To make the worker integration available through the interface, add the following to your viktor.config.toml:

worker_integrations = [
"civil3d",
]

Install the worker

Install a Civil 3D worker as a personal worker, on the machine where you use Civil 3D. It calls the Civil 3D API directly, so it needs no Python environment.

A personal worker connects the software that is installed on your own machine, for your own use. Personal workers are installed and managed with VIKTOR Desktop:

  1. Download and install VIKTOR Desktop and log in with your VIKTOR account, if you haven't done so already

  2. In VIKTOR Desktop, click "Add" and select Civil 3D

  3. Start the worker. You can view its logs inside VIKTOR Desktop, and the editor shows when your worker is online

note

Personal workers that were installed with the previous per-worker installer keep running, but new personal workers can only be added through VIKTOR Desktop: the per-worker installer download and connection-key flow are no longer available for personal use.

The Civil 3D worker also carries a units setting, metric by default or imperial. It decides which Civil 3D settings profile a connect() session starts in, and which instances attach() will bind to; see Create mode and Attach mode. Set it when you configure the worker.

Why there is no organization worker here

An organization worker shares one machine between the users of a workspace, which does not suit Civil 3D. The session drives the real desktop application, so every concurrent user needs their own interactive Windows session and their own licence, and attaching to "the instance already running" on a shared machine would reach whatever drawing the previous job left open.

To offer Autodesk capability across your organization instead of per user, use Autodesk Platform Services and its Design Automation API, which runs Autodesk workloads in Autodesk's cloud with no worker and no local install.

Testing

vkt.civil3d.connect needs to be mocked within the context of (automated) testing. The vkt.testing module provides the mock_Civil3DConnection decorator, which maps each COM path to the result it should return. The same decorator also mocks vkt.civil3d.attach:

import unittest

import viktor as vkt

from app.my_entity_type.controller import MyEntityTypeController


class TestMyEntityTypeController(unittest.TestCase):
@vkt.testing.mock_Civil3DConnection(results={
# a result shaped {"__handle__": n} is decoded into an object handle, as in production
'ActiveDocument.Points.Add': [{"__handle__": 1}],
'Easting': [1000.0],
'ActiveDocument.PipeNetworks.Count': [2],
})
def test_write_to_civil3d(self):
MyEntityTypeController().write_to_civil3d()

Paths that are not in the dictionary return None. For a result that depends on the call, pass a function of the COM path and its arguments instead of a dictionary.

Two things are specific to this mock:

  • The domain roots need no fixture. c3d.pipe and c3d.roadway are transport, answered by the mock itself, so results maps only the COM paths your app is visibly calling.
  • Results are keyed by path alone, without the root. c3d.pipe.ActiveDocument.PipeNetworks.Count() is keyed 'ActiveDocument.PipeNetworks.Count', which therefore collides with the same path reached from c3d itself. Key by the most specific path your test calls.

Troubleshooting the worker installation

How the worker finds the application

The autocad and civil3d worker kinds both use AutoCAD's automation registration, AutoCAD.Application. Civil 3D has no separate registration: it is AutoCAD started with Civil 3D's product switches, so both kinds start and connect to the same acad.exe.

At worker start, the worker checks that this registration points to an installed executable. If the version-independent entry is stale, the worker falls back to the newest versioned entry, such as AutoCAD.Application.26, and logs a warning. If no entry points to an installed executable, the kind refuses to start and asks you to install or repair the product.

Installation conflicts

Every AutoCAD-family product registers two entries: a versioned one per release, such as AutoCAD.Application.26 for the 2027 release, and the shared version-independent AutoCAD.Application. The shared entry is owned by whichever installer wrote it last, and uninstallers do not always clean it up. This is what can leave it stale or pointing at the wrong product:

  • An older release was removed after a newer one was installed. The shared entry keeps pointing at the removed release. The newer install registered its own versioned entry but did not rewrite the shared one. With the fallback the worker binds through the newest versioned entry and logs a warning. No action is needed, but repairing the installation cleans the entry up.
  • Several releases installed side by side. The worker starts the newest registered release, and for the civil3d kind that release must be a Civil 3D installation of a supported version. If a newer plain AutoCAD sits next to an older Civil 3D, the worker starts the plain AutoCAD and fails with "the instance this worker started did not load Civil 3D". Install one AutoCAD-family release per worker machine, or make sure the newest one is the Civil 3D release you want to automate.
  • Per-user registry overrides. Entries under the current user's classes take precedence over the machine-wide ones for a worker that runs without elevation. A leftover per-user entry can shadow a healthy machine-wide registration. Remove it, or repair the installation.
  • No registration at all. A failed or trial installation may not register the automation server. The worker refuses to start the kind with "No Civil 3D COM server is registered on this worker host. Install or repair Civil 3D."

In all cases the fix is on the machine, not in the worker. Repair the installation from Apps & Features, restart the worker, and check the worker log for the stale-registration warning.

Create mode

connect() starts a private instance for the session. Only one AutoCAD-family application can be automated on a machine at a time, so create mode refuses to start while any AutoCAD or Civil 3D window is already open on the worker machine. Close the open application, or use attach mode to work with it. This also means create sessions on one worker run one at a time.

Civil 3D create sessions start with the settings profile matching the worker's units setting, metric by default or imperial.

Attach mode

attach() connects to the instance the engineer already has open. Keep exactly one instance open. With several open, the worker reaches only the first one started and cannot pick another.

  • Nothing open. The session fails with reason "error_attach_no_instance". Open the model on the worker machine and retry.
  • Plain AutoCAD open, Civil 3D requested. Refused, with that same "error_attach_no_instance" reason: there is no attachable Civil 3D. Close AutoCAD, open the drawing in Civil 3D, and retry.
  • Civil 3D open, AutoCAD requested. Accepted. Civil 3D exposes the full AutoCAD API, so the autocad kind binds it happily.
  • Civil 3D open with the other units profile. Refused with "error_attach_wrong_product", and the message names both profiles. Change the worker's units setting, or restart Civil 3D from its metric or imperial shortcut.
  • Product started as administrator. An elevated instance is invisible to a worker that runs without elevation, and the session reports no running instance. Run the product and the worker as the same Windows user at the same elevation.

The worker never closes, hides, or saves the engineer's instance in attach mode.

Supported releases

The civil3d kind supports Civil 3D 2024 through 2027. An installed release outside that range fails at session start with a vkt.ExecutionError whose message lists the supported versions. The autocad kind has no release gate.

FAQs

Why does reading a property need parentheses?

Every attribute you touch on c3d is forwarded to Civil 3D, and the connection cannot tell "give me this value" from "give me this object to keep navigating" without the call. So a property is read with call syntax, point.Number(), and written by plain assignment, point.Description = "SET-OUT". A method is called the same way, which makes the rule simple in practice: anything you want a value back from gets parentheses. Getting it wrong raises a TypeError as soon as you use it as a value, before any round trip, with a hint naming the call you meant.

My points are all in the wrong place

Check the coordinate order. Points.Add takes Easting, Northing, Elevation — not Northing first, and not the [x, y, z] order you may be carrying through the rest of your app. Nothing raises when they are swapped; the points simply land somewhere else. Read Easting() and Northing() back in a test and assert they match what you sent.

Why can't I create a TIN surface?

Surfaces.AddTinSurface cannot be reached through a session. Its one parameter is an AeccTinCreationData object that no member of the API surface returns, so there is no way to build the argument from your app. This is a coverage boundary of Civil 3D's COM API rather than a restriction the worker adds.

What does work: reading an existing surface and its statistics (document.Surfaces.Item("EG").Statistics.MinElevation()), and Surfaces.ImportXML with a relative filename. Where a surface has to be created, create it in Civil 3D and have the app work against it.

Can I build a corridor from my app?

Mostly no. c3d.roadway.ActiveDocument gives you Corridors, Assemblies and Subassemblies, but Civil 3D's COM API exposes that domain as largely read-only. Read a corridor the engineer has built, and treat writing to it as something to verify against your own release before designing an app around it.

Why are Save and SendCommand refused?

The worker refuses members that let a session reach outside itself. Save would overwrite the user's file in place, so SaveAs with a relative filename is offered instead, which writes into the session's working directory. SendCommand, Run and GetInterfaceObject execute arbitrary code on the worker machine — and GetInterfaceObject is not needed anyway, because the worker hands you the domain roots directly. Civil 3D's Aecc objects inherit the AutoCAD surface, so these refusals hold on them too.

Can I download the DWG my app wrote?

Not in this release: no file can be returned from the session to the app, and the session's working directory goes away with the session. Where the point is to produce drawn output, use vkt.civil3d.attach() so the result lands in the drawing the user has open and they save it themselves. Where the point is a calculation, read the values you need through COM while the session is open and build your views from those.

My session fails with "the instance this worker started did not load Civil 3D"

The worker started an AutoCAD-family release that is not Civil 3D. Both products register as AutoCAD.Application, and the worker starts the newest registered release, so this happens when a newer plain AutoCAD is installed alongside an older Civil 3D. See Installation conflicts.

My Civil 3D release is not supported

The civil3d kind supports Civil 3D 2024 through 2027. A release outside that range fails at session start with a message listing the supported versions. If the machine has a supported release installed as well, check that it is the newest registered one, since that is the one the worker starts.

Every session times out and Civil 3D never appears

Check the two requirements in The worker before looking at your app code. The worker machine needs a logged-in, interactive Windows session, and Civil 3D must open straight to a drawing without an Autodesk sign-in or activation dialog. There is no way to check a licence up front, so a prompting licence surfaces only as a start-up failure.

Then check the timeout itself. timeout bounds one call, and for connect() the first of those has to cover Civil 3D booting, which takes minutes on a cold machine — hence the generous timeout=180 in the examples above. For attach() the timeout also bounds the attach itself, so keep it at or above 30 seconds.

How do I roll this out to everyone in my organization?

Not with an organization worker: it shares one machine between the users of a workspace, and each concurrent Civil 3D session needs its own interactive Windows session and licence. Give each engineer a personal worker if they each work in their own drawings.

If what you need is Autodesk capability without a per-user install, that is a different integration: Autodesk Platform Services offers a Design Automation API that runs Autodesk workloads in Autodesk's cloud, reached from a VIKTOR app through an OAuth 2.0 integration rather than through vkt.civil3d.

My app is slow when it reads a large drawing

Every call, property read, or property write is one round trip to the worker; navigating attributes to reach it is free. Collections are the usual culprit: Count() plus Item(i) plus one property read per item is three round trips per object, so a view over a drawing with thousands of points has to bound its work or look the objects up by name instead of sweeping the collection. Resolving c3d.ActiveDocument once outside the loop costs nothing either way, but it reads better.