# Add a storey

> A second floor with its own walls and slab, on top of the ground floor.

- Live example: https://floorkit-examples.ploterr.com/floorkit/guides/add-a-storey
- Docs page: https://docs.ploterr.com/floorkit/guides/add-a-storey
- Category: Model a building
- API: `BuildingSession`, `BuildingModel`
- Tags: storey, building model
- Package: `@opengeometry/floorkit`
- Source: `examples/src/guides/add-a-storey.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/add-a-storey.example.ts

```ts
import type { ExampleContext, ExampleMeta, ExampleResult } from '../app/lib/example';
import { addDoor } from './code/add-a-door';
import { addFloorSlab } from './code/add-a-floor-slab';
import { addStorey } from './code/add-a-storey';
import { addWindow } from './code/add-a-window';
import { drawRoom } from './code/draw-a-wall';
import { closeBuilding, startBuilding } from './code/start-a-building';

export const meta: ExampleMeta = {
  title: 'Add a storey',
  description: 'A second floor with its own walls and slab, on top of the ground floor.',
  tags: ['storey', 'building model'],
  api: ['BuildingSession', 'BuildingModel'],
  camera: { position: [14, 10, 13], target: [3, 3, 2] },
};

export function setup({ scene }: ExampleContext): ExampleResult {
  const session = startBuilding(scene);
  drawRoom(session);
  addDoor(session);
  addWindow(session);
  addFloorSlab(session);
  addStorey(session);
  return {
    expose: {
      get session() {
        return session;
      },
    },
    dispose: () => closeBuilding(scene, session),
  };
}
```

### 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-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-storey.ts

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

export const firstFloor: BmStorey = {
  id: 'first',
  name: 'First floor',
  elevation: 3,
  height: 3,
  walls: [
    {
      id: 'first-walls',
      points: [
        [0, 0],
        [6, 0],
        [6, 4],
        [0, 4],
      ],
      closed: true,
      thickness: 0.2,
    },
  ],
  slabs: [
    {
      id: 'first-floor-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 addStorey(session: BuildingSession): void {
  session.apply(
    { kind: 'add-storey', buildingId: 'building', storey: firstFloor },
    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/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/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();
}
```
