# Export a PDF

> The sheet of the house with its plan, section, elevation, dimension and note. The button downloads the same sheet as a PDF with vector linework.

- Live example: https://floorkit-examples.ploterr.com/floorkit/guides/export-a-pdf
- Docs page: https://docs.ploterr.com/floorkit/guides/export-a-pdf
- Category: Draw and export
- API: `ProjectManager`, `ProjectFile`
- Tags: pdf, export, sheet
- Package: `@opengeometry/floorkit`
- Source: `examples/src/guides/export-a-pdf.example.ts`

Each example is one module exporting `meta` and `setup(ctx)`. The examples shell renders it in 3D and as a 2D plan (or on a sheet surface for drawing examples) and builds its controls from the element schema.

## Source

### examples/src/guides/export-a-pdf.example.ts

```ts
import * as THREE from 'three';
import type { ExampleMeta, ExampleResult, SurfaceContext } from '../app/lib/example';
import {
  addAnnotation,
  addSheet,
  addViewport,
  elevationViewport,
  numberSectionMark,
  roomWidth,
  sectionViewport,
  showSheet,
  siteNote,
} from './code/compose-a-sheet';
import { downloadPdf, openSheets } from './code/export-a-pdf';
import { closeBuilding } from './code/start-a-building';
import { action, buildHouse, fitSheet, houseDrawings } from './support';

export const meta: ExampleMeta = {
  title: 'Export a PDF',
  description:
    'The sheet of the house with its plan, section, elevation, dimension and note. The button downloads the same sheet as a PDF with vector linework.',
  tags: ['pdf', 'export', 'sheet'],
  api: ['ProjectManager', 'ProjectFile'],
  surface: 'dom',
};

export async function setup({ container }: SurfaceContext): Promise<ExampleResult> {
  const scene = new THREE.Scene();
  const session = buildHouse(scene);
  const viewports = addViewport(
    addViewport(addSheet(houseDrawings(session)), sectionViewport),
    elevationViewport,
  );
  const project = addAnnotation(addAnnotation(numberSectionMark(viewports), roomWidth), siteNote);
  closeBuilding(scene, session);
  const manager = openSheets(project);
  const renderer = await showSheet(container, () => project);
  const stopFit = fitSheet(renderer, container);
  return {
    controls: [action('download-pdf', 'Download the PDF', () => downloadPdf(manager))],
    expose: { session: manager.session, project, manager },
    dispose: () => {
      stopFit();
      renderer.dispose();
      manager.dispose();
    },
  };
}
```

### examples/src/app/lib/example.ts

```ts
import type * as THREE from 'three';
import type { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import type { createModelRenderer, EditorChange, OPControl, VectorDrawing, WallNetwork } from '@opengeometry/floorkit';

export type EditTarget = THREE.Object3D & { ogType: string };

export type PlanRenderer = Awaited<ReturnType<typeof createModelRenderer>>;

export interface ExampleMeta {
  title: string;
  description: string;
  tags?: readonly string[];
  api?: readonly string[];
  camera?: { position: [number, number, number]; target: [number, number, number] };
  view?: '3d' | '2d';
  surface?: 'dom';
  grid?: false | { size: number; y?: number };
  notes?: readonly string[];
}

export interface SurfaceContext {
  container: HTMLElement;
  refresh: () => void;
}

export interface ExampleContext extends SurfaceContext {
  scene: THREE.Scene;
  camera: THREE.PerspectiveCamera;
  controls: OrbitControls;
  renderer: THREE.WebGLRenderer;
  onFrame: (callback: () => void) => void;
}

export interface PlanSource {
  getDrawing: () => VectorDrawing;
  getElementById?: (ogid: string) => unknown;
  getChangedDrawing?: (ogids: readonly string[]) => VectorDrawing;
  subscribeInvalidation?: (callback: (ogids: readonly string[]) => void) => () => void;
}

export interface PlanElement {
  ogid: string;
}

export type InputControl =
  | { kind: 'text'; input: 'date' | 'password' }
  | { kind: 'file'; accept: string };

export interface ValueControl {
  key: string;
  label: string;
  group?: string;
  control: OPControl | InputControl;
  get: () => unknown;
  set?: (value: unknown) => void;
  commit?: (value: unknown) => void;
}

export type ActionIcon = 'undo' | 'redo';

export interface ActionControl {
  key: string;
  label: string;
  group?: string;
  icon?: ActionIcon;
  run: () => unknown;
}

export type ControlOverride = ValueControl | ActionControl;

export type Verdict = 'pass' | 'fail';

export interface ReportSection {
  title: string;
  verdict?: Verdict;
  columns?: readonly string[];
  rows?: ReadonlyArray<{ cells: ReadonlyArray<string | number>; verdict?: Verdict }>;
  note?: string;
}

export interface EditChoice {
  label: string;
  target: EditTarget;
}

export interface ExampleResult {
  edit?: EditTarget | (() => EditTarget | undefined);
  targets?: readonly EditChoice[] | (() => readonly EditChoice[]);
  pick?: boolean;
  plan?: WallNetwork | readonly PlanElement[] | PlanSource;
  controls?: false | readonly ControlOverride[] | ((target: EditTarget | undefined) => readonly ControlOverride[]);
  report?: () => readonly ReportSection[];
  onEdit?: (change: EditorChange) => void;
  onChange?: () => void;
  onPlan?: (renderer: PlanRenderer) => void;
  expose?: Record<string, unknown>;
  dispose?: () => void;
}

export interface ExampleModule {
  meta: ExampleMeta;
  setup: (context: ExampleContext) => Promise<ExampleResult> | ExampleResult;
}
```

### examples/src/guides/code/compose-a-sheet.ts

```ts
import type { ProjectFile, Sheet } from '@opengeometry/floorkit';

export const planSheet: Sheet = {
  id: 'sheet-a101',
  name: 'House drawings',
  number: 'A-101',
  page: {
    format: 'A3',
    orientation: 'landscape',
    size: { width: 420, height: 297 },
    margin: 10,
  },
  viewports: [
    {
      id: 'plan-viewport',
      viewId: 'ground-plan',
      drawingNumber: '1',
      frame: { x: 20, y: 20, width: 200, height: 120 },
      scale: 0.02,
      rotationDeg: 0,
      clip: true,
      title: { text: 'Ground floor plan', showScale: true },
    },
  ],
  annotations: [],
  titleblock: { templateId: 'default', values: { sheetNumber: 'A-101' } },
};

export function addSheet(project: ProjectFile): ProjectFile {
  return { ...project, paper: { ...project.paper, sheets: [...project.paper.sheets, planSheet] } };
}

export function changeSheet(project: ProjectFile, change: (sheet: Sheet) => Sheet): ProjectFile {
  return {
    ...project,
    paper: {
      ...project.paper,
      sheets: project.paper.sheets.map((sheet) =>
        sheet.id === planSheet.id ? change(sheet) : sheet,
      ),
    },
  };
}

import type { Viewport } from '@opengeometry/floorkit';

export const sectionViewport: Viewport = {
  id: 'section-viewport',
  viewId: 'section-a',
  drawingNumber: '2',
  frame: { x: 235, y: 20, width: 165, height: 140 },
  scale: 0.02,
  rotationDeg: 0,
  clip: true,
  title: { text: 'Section A', showScale: true },
};

export const elevationViewport: Viewport = {
  id: 'elevation-viewport',
  viewId: 'elevation-south',
  drawingNumber: '3',
  frame: { x: 20, y: 160, width: 200, height: 85 },
  scale: 0.01,
  rotationDeg: 0,
  clip: true,
  title: { text: 'South elevation', showScale: true },
};

export function addViewport(project: ProjectFile, viewport: Viewport): ProjectFile {
  return changeSheet(project, (sheet) => ({ ...sheet, viewports: [...sheet.viewports, viewport] }));
}

export function setScale(project: ProjectFile, viewportId: string, scale: number): ProjectFile {
  return changeSheet(project, (sheet) => ({
    ...sheet,
    viewports: sheet.viewports.map((viewport) =>
      viewport.id === viewportId ? { ...viewport, scale } : viewport,
    ),
  }));
}

export function numberSectionMark(project: ProjectFile): ProjectFile {
  return {
    ...project,
    markers: (project.markers ?? []).map((marker) =>
      marker.id === 'section-a-mark'
        ? {
            ...marker,
            target: { viewId: 'section-a', sheetId: 'sheet-a101', viewportId: 'section-viewport' },
          }
        : marker,
    ),
  };
}

import type { Annotation } from '@opengeometry/floorkit';

export const roomWidth: Annotation = {
  id: 'room-width',
  kind: 'measure',
  viewportId: 'plan-viewport',
  from: { x: 0, y: 4 },
  to: { x: 6, y: 4 },
  offset: 10,
  fontSize: 2.5,
};

export const siteNote: Annotation = {
  id: 'site-note',
  kind: 'text',
  text: 'Check the door width on site.',
  position: { x: 240, y: 200 },
  fontSize: 4,
};

export function addAnnotation(project: ProjectFile, annotation: Annotation): ProjectFile {
  return changeSheet(project, (sheet) => ({
    ...sheet,
    annotations: [...sheet.annotations, annotation],
  }));
}

import {
  TitleblockRegistry,
  createSheetRenderer,
  drawProjectView,
  type PaperScreenRenderer,
} from '@opengeometry/floorkit';

export async function showSheet(
  container: HTMLElement,
  project: () => ProjectFile,
): Promise<PaperScreenRenderer> {
  const renderer = await createSheetRenderer();
  renderer.mount(container, project().paper, {
    resolveSavedView: (viewId) => drawProjectView(project(), viewId),
    extractor: { extract: () => drawProjectView(project(), 'ground-plan') },
    templates: new TitleblockRegistry(),
    projectName: 'Garden house',
  });
  return renderer;
}

import type { PaperEditEvent } from '@opengeometry/floorkit';

export function applyMove(project: ProjectFile, event: PaperEditEvent): ProjectFile {
  return changeSheet(project, (sheet) => ({
    ...sheet,
    viewports: sheet.viewports.map((viewport) =>
      viewport.id === event.targetId && event.frame
        ? { ...viewport, frame: event.frame }
        : viewport,
    ),
    annotations: sheet.annotations.map((annotation) =>
      annotation.id === event.targetId && event.annotation ? event.annotation : annotation,
    ),
  }));
}

export function followEdits(
  renderer: PaperScreenRenderer,
  read: () => ProjectFile,
  write: (project: ProjectFile) => void,
): void {
  renderer.on('move', (event) => {
    write(applyMove(read(), event));
    renderer.update(read().paper);
  });
  renderer.on('create', (event) => {
    if (!event.annotation) return;
    write(addAnnotation(read(), event.annotation));
    renderer.update(read().paper);
  });
}

export function setSheetStyle(
  renderer: PaperScreenRenderer,
  style: 'iso-128' | 'ink-sketch' | 'graphite' | 'napkin' | undefined,
): void {
  renderer.setPaperStyle?.(style);
}
```

### examples/src/guides/code/export-a-pdf.ts

```ts
import { ProjectManager, type ProjectFile } from '@opengeometry/floorkit';

export function openSheets(project: ProjectFile): ProjectManager {
  return new ProjectManager(project);
}

export async function downloadPdf(manager: ProjectManager): Promise<void> {
  const pdf = await manager.exportPdf();
  const url = URL.createObjectURL(pdf);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'drawings.pdf';
  link.click();
  URL.revokeObjectURL(url);
}

export function exportOneSheet(manager: ProjectManager): Promise<Blob> {
  return manager.exportPdf(['sheet-a101']);
}

export function exportWithTitle(manager: ProjectManager): Promise<Blob> {
  return manager.exportPdf(undefined, {
    projectName: 'Garden house',
    date: '2026-10-06',
  });
}
```

### examples/src/guides/code/start-a-building.ts

```ts
import * as THREE from 'three';
import { BuildingSession, type BuildingModel } from '@opengeometry/floorkit';

export const emptyBuilding: BuildingModel = {
  schema: 'floorkit.building-model',
  version: 3,
  units: 'meters',
  upAxis: 'y',
  id: 'my-building',
  buildings: [
    {
      id: 'building',
      storeys: [{ id: 'ground', name: 'Ground floor', elevation: 0, height: 3 }],
    },
  ],
};

export function startBuilding(scene: THREE.Scene): BuildingSession {
  const session = new BuildingSession(emptyBuilding);
  scene.add(session.root);
  return session;
}

import type { ElementLists } from '@opengeometry/floorkit';

export function removeElement(
  session: BuildingSession,
  list: keyof ElementLists,
  elementId: string,
): void {
  session.apply(
    { kind: 'remove-element', buildingId: 'building', storeyId: 'ground', list, elementId },
    session.revision,
  );
}

export function replaceBuilding(
  scene: THREE.Scene,
  current: BuildingSession,
  change: (model: BuildingModel) => void,
): BuildingSession {
  const model = current.snapshot();
  change(model);
  const next = new BuildingSession(model);
  scene.remove(current.root);
  current.dispose();
  scene.add(next.root);
  return next;
}

export function closeBuilding(scene: THREE.Scene, session: BuildingSession): void {
  scene.remove(session.root);
  session.dispose();
}
```

### examples/src/guides/support.ts

```ts
import type * as THREE from 'three';
import {
  drawProjectView,
  type BmStorey,
  type BuildingSession,
  type PaperScreenRenderer,
  type ProjectFile,
} from '@opengeometry/floorkit';
import type { ActionControl, ActionIcon, PlanSource } from '../app/lib/example';
import { addDoor } from './code/add-a-door';
import { addFloorSlab } from './code/add-a-floor-slab';
import { addRoof } from './code/add-a-roof';
import { addWindow } from './code/add-a-window';
import { createProject } from './code/create-a-plan-view';
import { addSectionMark, addSectionViews } from './code/cut-a-section';
import { drawRoom } from './code/draw-a-wall';
import { startBuilding } from './code/start-a-building';

export function groundOf(session: BuildingSession): BmStorey | undefined {
  return session.snapshot().buildings[0]?.storeys.find((storey) => storey.id === 'ground');
}

export function hasWall(session: BuildingSession, wallId: string): boolean {
  return groundOf(session)?.walls?.some((wall) => wall.id === wallId) ?? false;
}

export function hasOpening(session: BuildingSession, wallId: string, openingId: string): boolean {
  return (
    groundOf(session)
      ?.walls?.find((wall) => wall.id === wallId)
      ?.openings?.some((opening) => opening.id === openingId) ?? false
  );
}

export function planOf(session: () => BuildingSession): PlanSource {
  return { getDrawing: () => drawProjectView(createProject(session().snapshot()), 'ground-plan') };
}

export function action(
  key: string,
  label: string,
  run: () => unknown,
  icon?: ActionIcon,
): ActionControl {
  return icon ? { key, label, icon, run } : { key, label, run };
}

export function buildHouse(scene: THREE.Scene): BuildingSession {
  const session = startBuilding(scene);
  drawRoom(session);
  addDoor(session);
  addWindow(session);
  addFloorSlab(session);
  addRoof(session);
  return session;
}

export function houseDrawings(session: BuildingSession): ProjectFile {
  return addSectionMark(addSectionViews(createProject(session.snapshot())));
}

export function fitSheet(renderer: PaperScreenRenderer, container: HTMLElement): () => void {
  const sizes = new ResizeObserver(() => {
    renderer.resize(container.clientWidth, container.clientHeight);
    renderer.fitToPage();
  });
  sizes.observe(container);
  return () => sizes.disconnect();
}
```

### examples/src/guides/code/add-a-door.ts

```ts
import type { BuildingSession } from '@opengeometry/floorkit';

export function addDoor(session: BuildingSession): void {
  session.apply(
    {
      kind: 'add-hosted-opening',
      buildingId: 'building',
      storeyId: 'ground',
      wallId: 'room-walls',
      opening: {
        id: 'entry-door',
        kind: 'door',
        offset: 2,
        width: 0.9,
        height: 2.1,
        doorQuadrant: 1,
      },
    },
    session.revision,
  );
}

export function moveDoor(session: BuildingSession): void {
  session.apply(
    {
      kind: 'move-hosted-opening',
      buildingId: 'building',
      storeyId: 'ground',
      wallId: 'room-walls',
      openingId: 'entry-door',
      offset: 3,
    },
    session.revision,
  );
}

export function removeDoor(session: BuildingSession): void {
  session.apply(
    {
      kind: 'remove-hosted-opening',
      buildingId: 'building',
      storeyId: 'ground',
      wallId: 'room-walls',
      openingId: 'entry-door',
    },
    session.revision,
  );
}

export function moveDoorToWall(session: BuildingSession): void {
  session.apply(
    {
      kind: 'rehost-hosted-opening',
      buildingId: 'building',
      storeyId: 'ground',
      wallId: 'room-walls',
      openingId: 'entry-door',
      toWallId: 'garden-wall',
      offset: 3,
    },
    session.revision,
  );
}
```

### examples/src/guides/code/add-a-floor-slab.ts

```ts
import type { BmSlab, BuildingSession } from '@opengeometry/floorkit';

export const groundFloor: BmSlab = {
  id: 'ground-floor',
  role: 'slab',
  profile: {
    outerRing: [
      [-0.1, -0.1],
      [6.1, -0.1],
      [6.1, 4.1],
      [-0.1, 4.1],
    ],
  },
  thickness: 0.2,
  baseOffset: -0.2,
};

export function addFloorSlab(session: BuildingSession): void {
  session.apply(
    {
      kind: 'add-element',
      buildingId: 'building',
      storeyId: 'ground',
      list: 'slabs',
      element: groundFloor,
    },
    session.revision,
  );
}

export function moveSlab(session: BuildingSession): void {
  session.apply(
    {
      kind: 'transform-slab',
      buildingId: 'building',
      storeyId: 'ground',
      slabId: 'ground-floor',
      origin: [0, 0],
      translation: [1, 0],
      yawDeg: 0,
    },
    session.revision,
  );
}
```

### examples/src/guides/code/add-a-roof.ts

```ts
import { RoofType, type BmRoof, type BuildingSession } from '@opengeometry/floorkit';

export const mainRoof: BmRoof = {
  id: 'main-roof',
  profile: {
    outerRing: [
      [-0.1, -0.1],
      [6.1, -0.1],
      [6.1, 4.1],
      [-0.1, 4.1],
    ],
  },
  roofType: RoofType.GABLE,
  pitch: 30,
  overhang: 0.3,
  thickness: 0.2,
  baseOffset: 3,
};

export function addRoof(session: BuildingSession): void {
  session.apply(
    {
      kind: 'add-element',
      buildingId: 'building',
      storeyId: 'ground',
      list: 'roofs',
      element: mainRoof,
    },
    session.revision,
  );
}
```

### examples/src/guides/code/add-a-window.ts

```ts
import type { BuildingSession } from '@opengeometry/floorkit';

export function addWindow(session: BuildingSession): void {
  session.apply(
    {
      kind: 'add-hosted-opening',
      buildingId: 'building',
      storeyId: 'ground',
      wallId: 'room-walls',
      opening: {
        id: 'south-window',
        kind: 'window',
        offset: 4.5,
        width: 1.2,
        height: 1.2,
        sillHeight: 0.9,
      },
    },
    session.revision,
  );
}
```

### examples/src/guides/code/create-a-plan-view.ts

```ts
import {
  drawProjectView,
  type BuildingModel,
  type ProjectFile,
  type SavedDrawingView,
} from '@opengeometry/floorkit';

export const groundPlan: SavedDrawingView = {
  id: 'ground-plan',
  name: 'Ground floor plan',
  kind: 'plan',
  storeyId: 'ground',
  cutHeightAboveStorey: 1.2,
};

export function createProject(buildingModel: BuildingModel): ProjectFile {
  return {
    schema: 'floorkit.project',
    version: 3,
    buildingModel,
    views: [groundPlan],
    paper: { version: 1, unitSystem: 'metric', sheets: [] },
  };
}

export function planLinework(project: ProjectFile) {
  const drawing = drawProjectView(project, 'ground-plan');
  console.log(`${drawing.primitives.length} primitives`, drawing.bounds);
  return drawing;
}
```

### examples/src/guides/code/cut-a-section.ts

```ts
import type { ProjectFile, SavedDrawingView } from '@opengeometry/floorkit';

export const sectionA: SavedDrawingView = {
  id: 'section-a',
  name: 'Section A',
  kind: 'section',
  line: { start: [4.5, -1], end: [4.5, 5] },
  lookSide: 'right',
  depth: 5,
  verticalCrop: { minY: -0.5, maxY: 6 },
  settings: { levelLabels: { datum: 0, unit: 'm' } },
};

export const southElevation: SavedDrawingView = {
  id: 'elevation-south',
  name: 'South elevation',
  kind: 'elevation',
  origin: [3, -2],
  lookDirection: [0, 1],
  crop: { left: -4, right: 4, bottom: -6, top: 0.5 },
};

export function addSectionViews(project: ProjectFile): ProjectFile {
  return { ...project, version: 3, views: [...project.views, sectionA, southElevation] };
}

import type { ProjectViewMarker } from '@opengeometry/floorkit';

export const sectionMark: ProjectViewMarker = {
  id: 'section-a-mark',
  kind: 'source',
  parentViewId: 'ground-plan',
  target: { viewId: 'section-a' },
};

export function addSectionMark(project: ProjectFile): ProjectFile {
  return { ...project, version: 3, markers: [...(project.markers ?? []), sectionMark] };
}
```

### examples/src/guides/code/draw-a-wall.ts

```ts
import type { BuildingSession } from '@opengeometry/floorkit';

export function drawWall(session: BuildingSession): void {
  session.apply(
    {
      kind: 'create-wall',
      buildingId: 'building',
      storeyId: 'ground',
      wall: {
        id: 'garden-wall',
        points: [
          [0, 7],
          [6, 7],
        ],
        thickness: 0.2,
      },
    },
    session.revision,
  );
}

export function drawRoom(session: BuildingSession): void {
  session.apply(
    {
      kind: 'create-wall',
      buildingId: 'building',
      storeyId: 'ground',
      wall: {
        id: 'room-walls',
        points: [
          [0, 0],
          [6, 0],
          [6, 4],
          [0, 4],
        ],
        closed: true,
        thickness: 0.2,
      },
    },
    session.revision,
  );
}

export function removeWall(session: BuildingSession): void {
  session.apply(
    { kind: 'delete-wall', buildingId: 'building', storeyId: 'ground', wallId: 'garden-wall' },
    session.revision,
  );
}
```
