Cookbook
Reference C# snippets against the Civil 3D .NET API — building blocks for authoring tickets. Recipes are the parameterised, proven operations; these are the raw ingredients.
01-mtext-note.cs
01-mtext-note.cs Add a text note to the drawing on a dedicated annotation layer. The simplest end-to-end payload — good for proving the pipeline (Phase 0).
// 01-mtext-note.cs
// Add a text note to the drawing on a dedicated annotation layer.
// The simplest end-to-end payload — good for proving the pipeline (Phase 0).
EnsureLayer("C-ANNO-NOTE", 2); // 2 = yellow (ACI)
var id = AddMText(
position: new Point3d(100.0, 50.0, 0.0),
text: "Reviewed and updated per ticket.",
height: 3.0,
rotation: 0.0,
layer: "C-ANNO-NOTE");
Log($"Added MText note, handle {id.Handle}.");
02-line-and-polyline.cs
02-line-and-polyline.cs Draw geometry on a layer: one line plus a closed polyline (e.g. a simple boundary). Centerline Closed rectangular boundary
// 02-line-and-polyline.cs
// Draw geometry on a layer: one line plus a closed polyline (e.g. a simple boundary).
EnsureLayer("C-ROAD-CNTR", 1); // red — centerline
EnsureLayer("C-PROP-BNDY", 3); // green — boundary
// Centerline
AddLine(new Point3d(0, 0, 0), new Point3d(200, 0, 0), layer: "C-ROAD-CNTR");
// Closed rectangular boundary
var corners = new List<Point2d>
{
new Point2d(0, -20),
new Point2d(200, -20),
new Point2d(200, 20),
new Point2d(0, 20),
};
var plId = AddPolyline(corners, closed: true, layer: "C-PROP-BNDY");
Log($"Drew centerline + boundary (polyline {plId.Handle}).");
03-block-insert.cs
03-block-insert.cs Insert a block reference that already exists in the drawing's block table. Throws (and aborts the ticket) if the block isn't defined — author should confirm the name first.
// 03-block-insert.cs
// Insert a block reference that already exists in the drawing's block table.
// Throws (and aborts the ticket) if the block isn't defined — author should confirm the name first.
const string blockName = "NORTH_ARROW";
var id = InsertBlock(
blockName: blockName,
position: new Point3d(250, 150, 0),
scale: 1.0,
rotation: 0.0,
layer: "C-ANNO-SYMB");
Log($"Inserted block '{blockName}' as {id.Handle}.");
04-civil-alignments-list.cs
04-civil-alignments-list.cs Civil 3D example: read every alignment in the drawing and drop a text summary (name + station range) as a stacked list. Demonstrates reaching Civil objects via CivilDoc and reading them through the same transaction (Tx).
// 04-civil-alignments-list.cs
// Civil 3D example: read every alignment in the drawing and drop a text summary
// (name + station range) as a stacked list. Demonstrates reaching Civil objects via CivilDoc
// and reading them through the same transaction (Tx).
EnsureLayer("C-ANNO-ALIGN", 4); // cyan
double y = 0.0;
int count = 0;
foreach (ObjectId algId in CivilDoc.GetAlignmentIds())
{
var al = (Alignment)Tx.GetObject(algId, OpenMode.ForRead);
AddMText(
position: new Point3d(0.0, y, 0.0),
text: $"{al.Name}: STA {al.StartingStation:0.00} – {al.EndingStation:0.00}",
height: 2.5,
layer: "C-ANNO-ALIGN");
y -= 5.0;
count++;
}
Log($"Listed {count} alignment(s).");
05-state-plane-points-not-file.cs
STATE PLANE = THE POINTS, NOT THE FILE. ============================================================================ When the user says "put this on state plane" / "is this on state plane", they mean: ARE THE ACTUAL COGO POINT COORDINATES real-world state plane values that register
// STATE PLANE = THE POINTS, NOT THE FILE.
// ============================================================================
// When the user says "put this on state plane" / "is this on state plane", they mean:
// ARE THE ACTUAL COGO POINT COORDINATES real-world state plane values that register
// to an aerial / the real site?
// They do NOT mean the drawing's assigned coordinate-system CODE (GAHP-WF etc).
//
// These are INDEPENDENT:
// - Db coordinate-system code = just a LABEL on the DWG. Setting it moves nothing.
// - COGO point Easting/Northing = where the geometry actually lives.
// A drawing can be labeled GAHP-WF while its points are on assumed/local/shifted coords
// that float over the aerial and match nothing. That is exactly the GUS.dwg case:
// the file said state plane, the points did not register to the real road.
//
// HOW TO ACTUALLY CHECK (don't compute lat/lon and trust it — verify against a known point):
// 1. Read the raw point Easting/Northing.
// 2. Compare to a KNOWN control monument N/E for the site, or overlay an aerial in CAD.
// 3. If the points don't land on the aerial, they are NOT on state plane regardless of
// the file's coord-system label.
//
// HOW TO PUT POINTS ON STATE PLANE (when asked):
// - Need a real-world N/E for at least one on-site monument (two for rotation).
// - Transform the points/geometry to land on those, then the file label matches reality.
// - Do NOT just set the coordinate-system code and call it done.
//
// Read raw point coords to inspect (this is the check, not a conversion):
var cd = CivilDoc;
foreach (ObjectId id in cd.CogoPoints) {
var cp = (Autodesk.Civil.DatabaseServices.CogoPoint)Tx.GetObject(id, OpenMode.ForRead);
Log($"#{cp.PointNumber} '{cp.RawDescription}' E={cp.Easting:F3} N={cp.Northing:F3} Z={cp.Elevation:F3}");
}
06-stateplane-to-latlon.md
State plane → lat/lon → Google Maps
# State plane → lat/lon → Google Maps
When the user wants a map pin for a drawing on state plane, convert the raw Easting/Northing with
**pyproj** — do NOT hand-roll the projection math (it landed in the ocean four times).
## The conversion
```python
from pyproj import Transformer
E, N = 2298732.33, 1491703.49 # Easting (X), Northing (Y) — raw drawing coords
lat, lon = Transformer.from_crs("EPSG:2240", "EPSG:4326", always_xy=True).transform(E, N)
print(f"https://www.google.com/maps?q={lat:.6f},{lon:.6f}")
```
`always_xy=True` matters — it takes (E, N) in and gives (lon, lat) out, so unpack as
`lon, lat = ...` if you don't reorder. Above it's written `lat, lon =` because the transform is
called with (E,N) and pyproj returns (lon,lat); assign carefully and sanity-check the pin.
## Georgia zones (US survey feet, NAD83)
| Zone | EPSG | Covers |
|------|------|--------|
| **GA West** | **2240** | Atlanta/metro, west GA — CONFIRMED for the Woodstock/Cherokee CONTRACT.dwg site |
| GA East | 2239 | east GA (Savannah side) |
Metre variants: GA West 26967, GA East 26966 — only if the drawing is in metres (rare here).
## Gotchas learned the hard way
- **"State plane" = the POINTS, not the file's coord-system label.** See
cookbook/05-state-plane-points-not-file.cs. A file labelled GA West can still have points that
don't register.
- If pyproj lands the pin in the wrong place, the points are on a REAL zone but SHIFTED (wrong
position) — that needs field control points to solve, not a different EPSG.
- The right EPSG must match the actual zone; guessing zones puts the pin hundreds of miles off.
- Verified: CONTRACT.dwg E=2298732.33 N=1491703.49 → 34.100696, -84.159567 (GA West 2240),
which the user confirmed correct.
07-read-unopened-dwg-sidedb.cs
READ A DWG THAT IS NOT OPEN IN CIVIL 3D (side database, read-only) ============================================================================ The plugin runs C# inside the live acad.exe, but the .NET API can open ANY dwg on disk as a separate in-memory database. The live session and the open drawing
// READ A DWG THAT IS NOT OPEN IN CIVIL 3D (side database, read-only)
// ============================================================================
// The plugin runs C# inside the live acad.exe, but the .NET API can open ANY dwg
// on disk as a separate in-memory database. The live session and the open drawing
// are never touched. Requires Civil 3D running with SOME drawing (plugin must be
// loaded), but NOT the specific file open.
//
// Use for: "check this file / where is it / what's in it" without the user opening it.
// Proven on GUS.dwg and CONTRACT.dwg this session.
//
// SAFE = read-only. You CAN also write + Save a closed file this way, but that is the
// "editing a file with nothing on screen" capability flagged as dangerous — only ever
// do it if the user explicitly asks, and tell them before saving.
string path = @"C:\path\to\file.dwg";
if (!System.IO.File.Exists(path)) { Log("not found: " + path); return; }
using (var sdb = new Database(false, true)) {
sdb.ReadDwgFile(path, System.IO.FileShare.Read, true, null); // FileShare.Read = don't lock it
Log($"INSUNITS={sdb.Insunits}");
using (var t = sdb.TransactionManager.StartTransaction()) {
var bt = (BlockTable)t.GetObject(sdb.BlockTableId, OpenMode.ForRead);
// extents (skip origin junk under ~1000 to avoid parcel-label 0,0 artifacts)
double minx=double.MaxValue,miny=double.MaxValue,maxx=double.MinValue,maxy=double.MinValue;
var ms = (BlockTableRecord)t.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead);
foreach (ObjectId id in ms) {
var e = t.GetObject(id, OpenMode.ForRead) as Autodesk.AutoCAD.DatabaseServices.Entity;
if (e == null) continue;
try { var ex = e.GeometricExtents;
if (ex.MinPoint.X>1000){ if(ex.MinPoint.X<minx)minx=ex.MinPoint.X; if(ex.MaxPoint.X>maxx)maxx=ex.MaxPoint.X; }
if (ex.MinPoint.Y>1000){ if(ex.MinPoint.Y<miny)miny=ex.MinPoint.Y; if(ex.MaxPoint.Y>maxy)maxy=ex.MaxPoint.Y; }
} catch { }
}
Log($"extents ({minx:F1},{miny:F1})-({maxx:F1},{maxy:F1}) centre ({(minx+maxx)/2:F1},{(miny+maxy)/2:F1})");
// ALL text across model + every layout + block attributes (site name/address lives on
// paper-space LAYOUTS, not model space — e.g. 'HUEY MAGOOS' was on the C2-SITE layout)
foreach (ObjectId btrId in bt) {
var btr = (BlockTableRecord)t.GetObject(btrId, OpenMode.ForRead);
string where = btr.Name;
if (btr.IsLayout) { var lay = (Layout)t.GetObject(btr.LayoutId, OpenMode.ForRead); where = "L:" + lay.LayoutName; }
foreach (ObjectId id in btr) {
var o = t.GetObject(id, OpenMode.ForRead);
string s = null;
if (o is MText mt) s = (mt.Contents ?? "").Replace("\\P"," / ");
else if (o is DBText tx) s = tx.TextString;
else if (o is BlockReference br && br.AttributeCollection != null) {
foreach (ObjectId aid in br.AttributeCollection) {
var ar = (AttributeReference)t.GetObject(aid, OpenMode.ForRead);
if ((ar.TextString ?? "").Trim().Length > 1) Log($" [{where}] ATTR {ar.Tag}=\"{ar.TextString.Trim()}\"");
}
continue;
}
// strip mtext formatting codes for clean reading
if (!string.IsNullOrWhiteSpace(s)) {
var clean = System.Text.RegularExpressions.Regex.Replace(s, @"\\[A-Za-z][^;\\]*;|[{}]", "");
if (clean.Trim().Length >= 3) Log($" [{where}] \"{clean.Trim()}\"");
}
}
}
t.Commit();
}
}
Log("side-db read complete (live drawing untouched).");
08-locate-a-site-from-a-dwg.md
Locate a site / get a map pin from a DWG
# Locate a site / get a map pin from a DWG
The reliable order of operations, learned across GUS.dwg (Macon) and CONTRACT.dwg (Woodstock)
this session. Don't skip to conversion — check what the file actually contains first.
## 1. Read the file (open or side-db)
- If it's the open drawing: read CogoPoints + all layout text directly.
- If it's NOT open: side database (cookbook/07-read-unopened-dwg-sidedb.cs).
- **Site name / address / project usually lives on a paper-space LAYOUT, not model space.**
'HUEY MAGOOS' was in a building-summary table on the C2-SITE layout, invisible in the survey.
## 2. Get the location by NAME first (this always works)
Pull road names, N/F adjoiner blocks, tax parcels, deed/plat book refs. These give you the
town/county even when coordinates are unreliable:
- GUS.dwg: BASS ROAD + PROVIDENCE BLVD, parcels K003-xxxx (Bibb) → Macon.
- CONTRACT.dwg: LAKE LAUREL DRIVE, parcels 137-xxx (Baldwin) → Milledgeville label,
but the COORDS resolved to Woodstock — so the name text and the coords can disagree.
## 3. Map pin — ONLY from coords that actually register
- Convert with pyproj + the correct zone (cookbook/06-stateplane-to-latlon.md). GA West = EPSG:2240.
- **Verify the pin lands where the drawing says.** If it lands in the ocean/wrong state, either the
zone is wrong (try the other GA zone) or the points are shifted (need field control).
- "State plane" means the POINTS register, not the file's coord-system label
(cookbook/05-state-plane-points-not-file.cs). GUS.dwg was labelled GA West but its points did NOT
register to the aerial — unsolvable from the file; needs shot control points.
## 4. When the file can't be solved
If there's no coordinate tie AND the points don't register, say so plainly and STOP guessing. The fix
is field work: shoot 3 control points (2 to solve translation+rotation, 1 as a check) on recoverable
monuments (found iron pins are ideal), placed in the drawing as CPP-coded points. Then match
drawing-coord ↔ real-coord to compute the shift. Do NOT fake a projection.
## Anti-patterns burned this session
- Hand-rolling the Transverse Mercator math → landed in the Atlantic 4×. Use pyproj.
- Guessing the zone → hundreds of miles off. Confirm the zone with the user or by which one lands right.
- Saying "the drawing is on state plane" because of the file label → the user's aerial overlay proved
the points didn't register. Check registration, not the label.