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.
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:
- The user performs an action that requires a Civil 3D analysis.
- Your app opens a session, and each ActiveX call it makes is sent to the worker as a task.
- The worker executes that one call against the live Civil 3D instance and returns its result.
- Your app uses the returned values to build its views, or draws the next piece of geometry.
- (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.
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:
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 aTypeErroras 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
withblock ends. - Collections are read one item at a time, and they are 0-based. Use
Count()andItem(i);Itemalso takes a name, sodocument.Surfaces.Item("EG")is a lookup rather than a scan. A Pythonforloop 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 withEasting(),Northing()andElevation(). 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-referencesApplication,DocumentandParentare refused, so keep your own reference toc3d.ActiveDocumentinstead 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.ImportXMLand 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), andSave,CloseandQuit. Save withSaveAsand a relative filename instead. Civil 3D'sAeccobjects inherit the AutoCAD surface, so the same refusals hold on them. - The enumerations live on
vkt.civil3d, covering the AutoCADAc*enumerations as well as Civil 3D'sAec*andAecc*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 AeccDrawingUnitTypegives you the same object under a shorter name. Note that they are not on the top-levelvktnamespace, sovkt.AeccDrawingUnitTyperaisesAttributeError. - 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:
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
c3dis the Land root. There is noc3d.land, and your app never callsGetInterfaceObjectto reach a domain: the worker owns that, and the member is refused.c3d.pipeis the pipe-network root. ItsActiveDocumentis the same drawing seen through the pipe domain, and holdsPipeNetworks.c3d.roadwayis the corridor root, whoseActiveDocumentcarriesCorridors,AssembliesandSubassemblies. 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.
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 useattach().- There is no automatic fallback in either direction. If you want one, write it yourself by catching
vkt.WorkerSessionAttachErrorand callingconnect().
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:
Saveis refused, andSaveAswrites into the session's working directory.
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:
reason | Meaning |
|---|---|
"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:
-
Download and install VIKTOR Desktop and log in with your VIKTOR account, if you haven't done so already
-
In VIKTOR Desktop, click "Add" and select Civil 3D

-
Start the worker. You can view its logs inside VIKTOR Desktop, and the editor shows when your worker is online
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.
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.pipeandc3d.roadwayare transport, answered by the mock itself, soresultsmaps 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 fromc3ditself. 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
civil3dkind 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.