# PDF Export TunnelCAD exports cross-sections and reports to PDF through a layout-driven engine. A layout is an ICAD Paper Space drawing — authored in ICAD itself — that carries tagged viewports, text, and rectangles. At plot time, TunnelCAD walks each tagged entity, invokes a matching data provider, and produces a PDF page through ICAD's plot pipeline. :::{note} Single-section + batch (multi-page) PDF export are both shipping. Multi-section sheet booklets aren't part of the PDF engine — when you need a multi-section sheet layout, use the Section DXF Atlas export (right-click a tunnel → *Export Sections to DXF…* → atlas mode) instead. The analytics, template, and volume PDF exports are planned to migrate to the layout engine in a future release. ::: ## Quick start 1. Open a project and select a section. 2. Choose **File → Export Section to PDF…** or click the **Export PDF** button on the Sections toolbar. 3. Pick a layout (every new project starts with a seeded `section-A4` default). 4. Pick an output filename. 5. Click OK. ## Architecture Four components separate cleanly: | Component | Unit | Responsibility | |---|---|---| | Storage | `Project/UTunnelProjectIO.pas`, `Print/UProjectLayouts.pas` | Layouts + assets live as XML subtrees inside the `.tun` project file | | Layout executor | `Print/ULayoutExecutor.pas` | Loads a layout, walks tagged entities, calls providers, plots via ICAD | | Provider registry | `Print/UProviderRegistry.pas` | `IProvider` interface + global `RegisterProvider` / `LookupProvider` | | ICAD plot bridge | `cad/PrintEngine/UCADPlotAPI.pas` | Thin `RenderCanvasToPDF` API that drives ICAD's existing plotter | ## Layouts A layout is a regular ICAD drawing authored in Paper Space. Entities can be tagged with two reserved attributes on any `TDrawObject`: - `provider` — the provider kind (e.g. `SECTION_VIEW`, `TEXT`, `DR_TABLE`) - `providerArgs` — `key=val;key=val` argument string parsed by `TArgMap` Untagged entities pass through as static layout content (title block lines, borders, company text). Layouts live inside the `.tun` project as `…` nodes under a `` subtree. Binary assets (logos, photos) live as base64-encoded `` nodes under ``. New projects are seeded with a default `section-A4` layout on creation. ## Built-in providers | Provider | Role | Target | Purpose | |---|---|---|---| | `TEXT` | paper | text entity | Substitute `%FIELD%` tokens (or replace entire text via `field=X`) from the plot context | | `SECTION_VIEW` | model | viewport | Draw all section line data into model space on layers named `LINE_` and frame the viewport | | `DR_TABLE` | paper | rectangle | Draw DR min/max/avg/std table (placeholder until analytics integration in Plan 3) | | `POLAR_CHART` | paper | rectangle | Concentric deviation polar grid + data polyline (placeholder as above) | | `IMAGE` | paper | rectangle | Insert a project asset (PNG / JPEG / BMP) at the slot rectangle as an embedded `TCADImage` | | `STATUS_STRIP` | paper | rectangle | Chainage bar with one grey tick per section in the alignment plus a red tick at the current section | ### Layout TEXT placeholder fields Layout text entities tagged with the `TEXT` provider can substitute project values either inline as `%FIELD%` tokens or whole-text via the `field=` argument. All numeric fields default to the project's unit settings (Tunnel Settings → Units); override decimals or format with `fmt=` on the slot's args. #### No-argument fields | Field | Source | Default format | |---|---|---| | `PROJECT_NAME` | Project name | string | | `TUNNEL` | Active alignment name | string | | `STATION` / `CHAINAGE` | Section station | `0.000` | | `SECTION_NAME` | Section name | string | | `TEMPLATE` | Section template name | string | | `PAGE_INDEX` / `PAGE_COUNT` | Layout pagination | integer | | `EASTING` / `NORTHING` | Alignment world coordinates at the station | project `CoordinateDecimals` | | `ELEVATION` | Alignment elevation | project `ElevationDecimals` | | `BEARING` | Alignment bearing at the station | project `AngularUnit` + `AngleDecimals` (e.g. `100.0000g`) | | `GRADIENT` | Vertical gradient at the station, percent | project `OffsetDecimals`, `%` suffix | | `RADIUS` | Horizontal radius at the station | project `CoordinateDecimals`; `inf` on straight runs | | `START_CHAINAGE` / `END_CHAINAGE` | Alignment chainage range | project `StationDecimals` | | `ALIGNMENT_LENGTH` | End − start chainage | project `CoordinateDecimals` | | `SECTION_INDEX` / `SECTION_COUNT` | 1-based index in the alignment / total sections | integer | | `POINT_COUNT` | Total measured points on the section (sum across all line definitions) | integer | | `DATE_NOW` / `TIME_NOW` | `Now`, override format via `fmt=` | `yyyy-mm-dd` / `hh:nn` | | `PROJECT_FILE` | Project file path; literal `(unsaved)` when the project has not been saved | string | #### Parameterised fields (`field=` mode only) These cannot be used inline as `%FIELD%`; set them via `field=` with the companion arg. | Field | Required arg | Description | |---|---|---| | `AREA` | `vol=` | Cross-section area (m²) for the named volume definition at the current section. | | `POINT_COUNT` | `line=` (optional) | Without `line=`, sums all lines (same as inline `%POINT_COUNT%`). With `line=`, returns only that one line's point count. | For cumulative or total volume across the whole alignment, use the **Mass Table** dialog (right-click a volume definition → *Mass Table…*). The dialog computes both values and exports them to CSV / XLSX / PDF. #### Tagging a text entity — step by step Two modes, picked per text entity: | Mode | When to use | |---|---| | **Inline `%FIELD%` substitution** (default) | The entity's existing text body holds a sentence with one or more `%TOKEN%` placeholders. The provider replaces only the tokens; everything else stays. | | **Whole-text `field=` mode** | The entity should be replaced *entirely* by one resolved value (with optional `prefix=` / `suffix=`). Required for parameterised tokens like `AREA` and `POINT_COUNT line=`. | ##### 1. Drop the text in the layout 1. Open the layout in the layout editor (paper-space tab on the **Sections** workspace). 2. Use the standard CAD **Text** tool to place a text entity where you want the label. Type the **default** body — this is what shows in the layout when no project is loaded, and (in inline mode) it's also the template the provider rewrites. ##### 2. Tag it as `TEXT` 1. Select the text entity on the paper canvas. 2. In the right-side **Provider Palette** panel, click `TEXT`. The palette's **args form** below the provider list switches to the TEXT schema (six widgets: `field`, `fmt`, `prefix`, `suffix`, `vol`, `line`). 3. Configure the args (next section). 4. Click **Tag Selected**. The entity now carries `Provider=TEXT` and the serialized args string. The **Outline** panel shows it; the **Property Inspector** has both rows. To re-edit a tag later, select the entity again — the palette auto-loads its current `Provider` + args. Change values, click **Tag Selected** to save. ##### 3. Configure the args form The args form widgets: | Widget | Type | Effect | |---|---|---| | `field` | dropdown of every supported field | **Empty** → inline `%FIELD%` substitution mode. **Set to a name** → whole-text replacement mode. | | `fmt` | text box | Optional override for numeric formatting. Examples: `0.000`, `0`, `0.00 m`. For `DATE_NOW` / `TIME_NOW` it's any FPC `FormatDateTime` pattern (`yyyy-mm-dd hh:nn`). Leave empty to use the project's unit defaults. | | `prefix` | text box | Prepended to the resolved value. **Only honoured in `field=` mode** — ignored when `field` is empty. | | `suffix` | text box | Appended to the resolved value. **Only honoured in `field=` mode**. | | `vol` | text box | The volume-definition name. Required when `field=AREA`; ignored otherwise. Case-insensitive. | | `line` | text box | The line-definition name. Optional when `field=POINT_COUNT` (without it, all lines are summed). Ignored when `field` is anything else. | ##### 4. Worked examples These are the most common patterns. The "ProviderArgs" column shows the serialized form the editor stores under the hood (`key=val;key=val`); in normal use you fill in widgets, you don't type the string. ###### Inline mode — leave `field` empty Type the tokens directly into the entity's text body. Tokens are uppercase and surrounded by `%`. | Entity text body | ProviderArgs | Resolved output (sample) | |---|---|---| | `Sta %STATION%` | *(empty)* | `Sta 125.500` | | `Sta %STATION%, %SECTION_NAME%` | *(empty)* | `Sta 125.500, S-15` | | `Tunnel: %TUNNEL%, axis %EASTING%E %NORTHING%N` | *(empty)* | `Tunnel: Bypass, axis 472301.250E 4218905.732N` | | `Bearing %BEARING%, gradient %GRADIENT%` | *(empty)* | `Bearing 195.4220g, gradient 1.500%` | | `Section %SECTION_INDEX% of %SECTION_COUNT%` | *(empty)* | `Section 12 of 47` | | `Plotted %DATE_NOW% by %PROJECT_NAME%` | *(empty)* | `Plotted 2027-05-07 by Bypass-East-Phase2` | The entity's body is the template. Any text outside `%…%` is preserved literally. ###### Whole-text mode — set `field` to a token name The entity's existing text body is **discarded** and replaced by `prefix + resolved + suffix`. | `field` | Other args | Resolved output | Use for | |---|---|---|---| | `STATION` | *(none)* | `125.500` | A station label that is *just* the station, no surrounding sentence. | | `STATION` | `prefix=Sta `, `suffix= m` | `Sta 125.500 m` | Same, with caption + unit. | | `BEARING` | *(none)* | `100.0000g` | Default project units (gradians here). | | `BEARING` | `fmt=0.00` | `100.00g` | Override decimals; the angular unit and `g` suffix come from the project. | | `RADIUS` | `prefix=R = `, `suffix= m` | `R = 250.000 m` *or* `R = inf m` on a straight run | Horizontal radius label. | | `DATE_NOW` | `fmt=dd/mm/yyyy` | `07/05/2027` | Custom date format. | | `AREA` | `vol=Excavation` | `87.530` | Per-section excavation area. | | `AREA` | `vol=Excavation`, `prefix=Area = `, `suffix= m²` | `Area = 87.530 m²` | Same with full caption. | | `POINT_COUNT` | *(none)* | `42` | Total points on the section. | | `POINT_COUNT` | `line=Crown` | `12` | Just the Crown line's points. | | `POINT_COUNT` | `line=Crown`, `prefix=Crown: `, `suffix= pts` | `Crown: 12 pts` | Caption + unit. | ###### When you need multiple parameterised values on one label You can't write `%AREA[Excavation]%` inline (parameterised tokens are field-mode only). To show, e.g., excavation **and** lining areas on the same drawing, drop **two** text entities side by side: - Entity A: `field=AREA`, `vol=Excavation`, `prefix=Excavation: `, `suffix= m²` - Entity B: `field=AREA`, `vol=Lining`, `prefix=Lining: `, `suffix= m²` ##### 5. Troubleshooting | Symptom | Cause | Fix | |---|---|---| | The token shows up literally as `%STATION%` in the PDF | Inline mode but the token is misspelled, lower-case, or uses an unsupported name | Tokens are **uppercase** and must match the supported list. The PDF export emits a warning diagnostic listing the offending field. | | Field-mode entity shows the original text instead of the resolved value | `field=` is empty (still in inline mode) | Set the `field` dropdown to the desired token. | | `%AREA%` shows up literally | `AREA` is a parameterised token; inline `%AREA%` is intentionally not supported | Switch to `field=AREA` mode and fill `vol=` with the volume-definition name. | | Empty text where you expected an area / radius / bearing | The alignment has no engine geometry yet, or `vol=` references a name that doesn't exist | Import the alignment geometry; verify the volume-definition name in *Tunnel Settings → Volumes*. | | `fmt=0.000` ignored on `BEARING` | `fmt=` only overrides decimals; the angular unit stays from project units | Change *Tunnel Settings → Units → Angular Unit* if you want degrees / DMS / radians instead of gradians. | ### Tagging an IMAGE entity — step by step The `IMAGE` provider drops a project-embedded raster (logo, photo, scan) into the slot rectangle. ##### 1. Register the image as a project asset Images live in the project's **Assets** collection — named binary blobs serialized inside the `.tun` file, so the layout travels with the project. 1. Open the asset manager from one of: - Ribbon **File → Manage Project Assets…** - Right-click the project root in the **Project Tree → *Manage Project Assets…*** 2. Click **Add…** → pick a PNG / JPEG / BMP file. - The MIME type is auto-detected from the file extension. - The asset name defaults to the **filename stem** (e.g. `company_logo.png` → asset name `company_logo`). - On a name collision the dialog prompts **Replace / Rename / Cancel**. 3. The list view shows every registered asset with name, MIME type, and size in KB. **Rename** and **Remove** operate on the selected row. 4. Close the dialog. Save the project (the assets are embedded as base64 in the `.tun`). ##### 2. Drop a placeholder rectangle in the layout In the layout editor, draw a regular CAD rectangle where the image should appear. The `IMAGE` provider sizes the bitmap to the rectangle's bounds (no scaling preserves aspect — make the rectangle the intended display size). ##### 3. Tag the rectangle as `IMAGE` 1. Select the rectangle. 2. In the Provider Palette, click `IMAGE`. The args form has one widget: **`asset`**. 3. Type the asset name you registered in step 1 (e.g. `company_logo`) — case-insensitive. 4. Click **Tag Selected**. The rectangle now resolves to your bitmap on the next plot. The bottom-left of the rectangle becomes the bottom-left of the image; the rectangle's width and height become the image's world width and height. ##### 4. Worked examples | ProviderArgs | Effect | |---|---| | `asset=company_logo` | Inserts the asset named `company_logo` into the slot rectangle. | | `asset=` (empty) | Warning — `IMAGE` requires `asset=`; nothing is drawn. | | `asset=missing` (name not in the asset list) | Warning — `missing asset: missing`; the placeholder X-rectangle is drawn so you can still see the slot. | If the asset's MIME isn't one of `image/png` / `image/jpeg` / `image/bmp`, the provider draws the placeholder rectangle and emits a `unsupported MIME type` diagnostic. ## Editing layouts on the Sections tab Layouts are paper-space tabs on the existing **Sections** tab workspace. Model space holds the current section's geometry (auto-updated as you navigate); paper tabs compose the PDF output. 1. Open a project. The **Sections** tab shows the current section in model space. A **Layouts** side panel docks on the right. 2. **Layouts → New Layout…** creates an empty paper tab; **Layouts → Load Layout…** loads a dormant layout already in the project. 3. Flip to the paper tab on the workspace tab strip. The side panel's palette + args form become active. 4. Draw entities on the paper tab with standard CAD tools, or click **Insert** on the palette to drop a default-sized placeholder. Tag existing entities via **Tag Selected**. 5. Fill in provider arguments in the schema-driven form. **Insert** and **Tag Selected** both read the current args. 6. The side panel's **Outline** lists every tagged entity. The loaded-layouts list lets you unload without going through the menu. 7. Navigate to another section — tagged entities on every loaded paper tab re-resolve automatically (live WYSIWYG). The viewport auto-frames the new model geometry. 8. **Layouts → Export Section to PDF…** (or `TUNPDF`) plots the active paper tab. **Layouts → Batch Export PDF…** (or `TUNPDFBATCH`) plots a station range as a single multi-page PDF. 9. The **Property Inspector** shows `Provider` and `ProviderArgs` rows for any selected entity — edit tags there too. 10. Save the project — loaded layouts capture their live state into their `IQMLBlob`s; dormant layouts pass through unchanged. ## Known limitations - **DR data (for `DR_TABLE` and `POLAR_CHART`) is not wired.** Radial deviations come from the analytics engine; Plan 3 will integrate them. For now both providers emit a warning and draw a placeholder frame. - **No ICAD command-registry integration yet.** `TUNPDF` / `TUNPDFBATCH` / `TUNLAYOUT*` are plain procedures invoked from menu items; typing them at the command line isn't wired until TunnelCAD adopts ICAD's command registry. - **Sheet sets / per-section booklet output are out of scope.** Use the Section DXF Atlas export (right-click a tunnel → *Export Sections to DXF…* → atlas mode) when you need a multi-section sheet layout. ## Verifying the editor (manual smoke test) After building TunnelCAD locally: 1. Open TunnelCAD, create a new project → Sections tab active, Layouts side panel visible, no layouts loaded. 2. **Layouts → New Layout…** → type a name → empty paper tab appears on the workspace tab strip. 3. Flip to the paper tab. Click `DR_TABLE` in the palette → click **Insert** → a rectangle appears tagged with `DR_TABLE`. The outline lists it. The inspector shows `Provider=DR_TABLE`. 4. Navigate to a section that has data → the DR_TABLE rectangle refreshes (placeholder content until Plan 3 wires analytics). 5. **Layouts → Export Section to PDF…** → pick an output path → a valid single-page PDF is produced. 6. **Layouts → Batch Export PDF…** → pick the loaded layout + a station range → a multi-page PDF is produced. 7. Save project, close, reopen → the layout is present in the loader picker (dormant). **Layouts → Load Layout…** restores it with the same content. ## Commands - `TUNPDF` — plot the active paper tab on the Sections workspace to PDF. Wired as `ExecuteTunPDFCommand(Owner, Project, Section, Session)` in `Print/UCmdTunPDF.pas`. - `TUNPDFBATCH` — plot a station range as a single multi-page PDF. Wired as `ExecuteTunPDFBatch(Owner, Project, Session, Navigate)` in `Print/UCmdLayouts.pas`. - `TUNLAYOUTLOAD` / `TUNLAYOUTUNLOAD` / `TUNLAYOUTNEW` / `TUNLAYOUTDELETE` / `TUNLAYOUTIMPORT` / `TUNLAYOUTEXPORT` — the load/unload lifecycle. ## Extending the engine To add a new provider — for example, a custom chart type: 1. Create a new `Print/UProviderX.pas` unit implementing `IProvider` (name, role, `Resolve(Ctx, Slot, Canvas, Args, Result, LayoutName)`). 2. Call `RegisterProvider(TProviderX.Create)` — usually from a `RegisterProviderX` procedure invoked at app startup. 3. Author a layout in ICAD with a tagged entity using your new provider name and args. 4. That's it — the engine picks it up automatically. See `Print/UProviderText.pas` and `Print/UProviderSectionView.pas` for worked examples of paper-role and model-role providers respectively. ## Design documents - Spec: `docs/superpowers/specs/2026-04-18-tunnel-pdf-export-design.md` - Plan 1: `docs/superpowers/plans/2026-04-18-tunnel-pdf-export-plan-1-engine-skeleton.md` - Layout Editor: `docs/superpowers/plans/2026-04-18-tunnel-layout-editor-plan.md`