# Save and reopen

> Save the project as text, change the building, then open the saved text again in a new session.

- Live example: https://floorkit-examples.ploterr.com/floorkit/guides/save-and-reopen
- Docs page: https://docs.ploterr.com/floorkit/guides/save-and-reopen
- Category: Draw and export
- API: `ProjectManager`
- Tags: save, open, project file
- Package: `@opengeometry/floorkit`
- Source: `examples/src/guides/save-and-reopen.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/save-and-reopen.example.ts

```ts
import type { ExampleContext, ExampleMeta, ExampleResult } from '../app/lib/example';
import { addDoor, moveDoor } from './code/add-a-door';
import { addWindow } from './code/add-a-window';
import { createProject } from './code/create-a-plan-view';
import { drawRoom } from './code/draw-a-wall';
import { closeManager, openManager, reopen, save } from './code/save-and-reopen';
import { emptyBuilding } from './code/start-a-building';
import { action, planOf } from './support';

export const meta: ExampleMeta = {
  title: 'Save and reopen',
  description:
    'Save the project as text, change the building, then open the saved text again in a new session.',
  tags: ['save', 'open', 'project file'],
  api: ['ProjectManager'],
  camera: { position: [12, 8, 11], target: [3, 1.5, 2] },
};

export function setup({ scene }: ExampleContext): ExampleResult {
  let manager = openManager(scene, createProject(emptyBuilding));
  drawRoom(manager.session);
  addDoor(manager.session);
  addWindow(manager.session);
  let saved: string | undefined;
  return {
    plan: planOf(() => manager.session),
    controls: [
      action('save', 'Save', () => {
        saved = save(manager);
      }),
      action('move-door', 'Move the door', () => moveDoor(manager.session)),
      action('reopen', 'Reopen the saved text', () => {
        if (saved === undefined) return;
        manager = reopen(scene, manager, saved);
      }),
    ],
    report: () => [
      {
        title: 'Project file',
        columns: ['State', 'Value'],
        rows: [
          {
            cells: ['Saved text', saved === undefined ? 'not saved' : `${saved.length} characters`],
          },
          { cells: ['Session revision', manager.session.revision] },
        ],
      },
    ],
    expose: {
      get session() {
        return manager.session;
      },
      get manager() {
        return manager;
      },
    },
    dispose: () => closeManager(scene, manager),
  };
}
```

### 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/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-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/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,
  );
}
```

### examples/src/guides/code/save-and-reopen.ts

```ts
import type * as THREE from 'three';
import { ProjectFileError, ProjectManager, type ProjectFile } from '@opengeometry/floorkit';

export function openManager(scene: THREE.Scene, project: ProjectFile): ProjectManager {
  const manager = new ProjectManager(project);
  scene.add(manager.root);
  return manager;
}

export function save(manager: ProjectManager): string {
  return manager.save();
}

export function closeManager(scene: THREE.Scene, manager: ProjectManager): void {
  scene.remove(manager.root);
  manager.dispose();
}

export function reopen(scene: THREE.Scene, current: ProjectManager, text: string): ProjectManager {
  const next = ProjectManager.open(text);
  closeManager(scene, current);
  scene.add(next.root);
  return next;
}

export function tryOpen(text: string): ProjectManager | undefined {
  try {
    return ProjectManager.open(text);
  } catch (error) {
    if (error instanceof ProjectFileError) {
      console.error(`${error.code} at ${error.path}`);
      return undefined;
    }
    throw error;
  }
}
```

### 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-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/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] };
}
```
