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.