> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ploterr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Model a mass

> Block out a building as a simple volume, change it with commands, check the zoning rules, then turn it into walls.

export const LiveExample = ({path, title = 'Live example', height = 460}) => {
  return <Frame>
      <iframe src={`https://floorkit-examples.ploterr.com/floorkit/${path}?embed=1`} ref={frame => {
    if (!frame || !['localhost', '127.0.0.1'].includes(window.location.hostname)) return;
    const local = `http://localhost:5555/${path}?embed=1`;
    if (frame.src !== local) frame.src = local;
  }} title={title} loading="lazy" allow="fullscreen" style={{
    width: '100%',
    height: `${height}px`,
    border: 0,
    display: 'block'
  }} />
    </Frame>;
};

A mass is the building as a simple volume. It is one or more extruded outlines, stacked. Use a mass at the start of a design, before walls exist, to test the size, the height and the floor area.

For a complete site study with multiple buildings, zoning, feasibility estimates and option comparison, see the [Site massing playbook](/floorkit/playbooks/site-massing).

<LiveExample path="guides/model-a-mass" />

## Before you start

You have a session from [Start a building](/floorkit/guides/start-a-building). You use one session for the whole guide.

## Write the storeys and the mass

This building has three storeys. The mass has two parts: a podium for the ground storey and a smaller tower for the two storeys above it.

```typescript theme={null}
import {
  Building,
  irMassToMassLike,
  massToShell,
  type BmMass,
  type BmStorey,
  type BuildingModel,
  type BuildingSession,
} from '@opengeometry/floorkit';

export const storeys: BmStorey[] = [
  { id: 'ground', name: 'Ground floor', elevation: 0, height: 4, usage: 'retail' },
  { id: 'first', name: 'First floor', elevation: 4, height: 3, usage: 'residential' },
  { id: 'second', name: 'Second floor', elevation: 7, height: 3, usage: 'residential' },
];

export const mass: BmMass = {
  id: 'building-mass',
  elevation: 0,
  segments: [
    {
      id: 'podium',
      footprint: {
        outerRing: [
          [0, 0],
          [12, 0],
          [12, 8],
          [0, 8],
        ],
      },
      height: 4,
      use: 'retail',
      storeyIds: ['ground'],
    },
    {
      id: 'tower',
      footprint: {
        outerRing: [
          [2, 1],
          [10, 1],
          [10, 7],
          [2, 7],
        ],
      },
      height: 6,
      use: 'residential',
      storeyIds: ['first', 'second'],
    },
  ],
};
```

## Set the site and the mass

Both go into the session as commands. `replace-site` gives the building a lot and its zoning rules. `replace-mass` sets the mass and its storeys in one step. A storey that is not in the list is removed, and a new storey id adds an empty storey.

```typescript theme={null}
import { massSegmentHeightCommand, type ZoningRuleSet } from '@opengeometry/floorkit';

export const zoning: ZoningRuleSet = {
  id: 'guide-rules',
  rules: [
    { kind: 'ratio-cap', id: 'far', quantity: 'floorAreaRatio', max: 0.35 },
    { kind: 'envelope-plane', id: 'setback', edgeClass: 'all', offsetAtGrade: 3 },
  ],
};

export function setSiteAndMass(session: BuildingSession): void {
  session.apply(
    {
      kind: 'replace-site',
      site: {
        id: 'lot',
        boundary: {
          outerRing: [
            [-4, -4],
            [30, -4],
            [30, 14],
            [-4, 14],
          ],
        },
        zoning,
      },
    },
    session.revision,
  );
  session.apply({ kind: 'replace-mass', buildingId: 'building', mass, storeys }, session.revision);
}
```

The mass belongs to the building, not to a storey. Each part of the mass names the storeys that it contains. Floorkit builds the two volumes, and one undo takes back each step.

## Read the floor area

After the build, get the mass from the session and read its quantities.

```typescript theme={null}
export function grossFloorArea(session: BuildingSession): number | undefined {
  const element = session.currentBuild.elementsById.get('building-mass');
  return element instanceof Building ? element.quantities().grossFloorArea : undefined;
}
```

For this mass the result is 192 square metres: 96 for the podium and 96 for the two tower storeys.

| Quantity | Meaning | This mass |
| - | - | - |
| `footprintArea` | The area of the lowest part | 96 m² |
| `grossFloorArea` | The sum of the areas of all storeys | 192 m² |
| `grossFloorAreaByUse` | The floor area for each use | retail 96 m², residential 96 m² |
| `grossVolume` | The volume of all parts | 672 m³ |
| `height` | The height from the bottom to the top | 10 m |
| `numberOfStoreys` | The number of storeys in the mass | 3 |

## Change the mass

Each change below is one command and one undo step.

**Height.** `massSegmentHeightCommand` gives the `replace-mass` command for a new height of one part. The height snaps to whole storeys, at the pitch of the top storey. It adds storeys with new ids, or removes empty top storeys, and it stacks every storey above again. It returns `undefined` when the height does not change.

```typescript theme={null}
export function setTowerHeight(session: BuildingSession, height: number): void {
  const command = massSegmentHeightCommand(session.snapshot(), 'building', 'tower', height);
  if (command) session.apply(command, session.revision);
}
```

**Area kind.** A storey has an `areaKind`: `gross`, `balcony`, `core`, `basement`, `parking` or `mechanical`. A `parking` storey does not count toward the floor area ratio, unless the zoning rules list that kind in `countsTowardFAR`. `replace-storey-settings` sets it. It replaces all the settings of the storey, so give them all.

```typescript theme={null}
export function makeGroundParking(session: BuildingSession): void {
  session.apply(
    {
      kind: 'replace-storey-settings',
      buildingId: 'building',
      storeyId: 'ground',
      settings: {
        name: 'Ground floor',
        elevation: 0,
        height: 4,
        usage: 'parking',
        areaKind: 'parking',
      },
    },
    session.revision,
  );
}
```

**Colour.** Each part has a colour. A part takes its own `color` (`#rrggbb`), else the `color` of the mass, else the colour of its use (the `use` of the part, else of the mass, else the `buildingType` of the building), else `#a0b8c8`. The colours by use are in `MASS_COLORS_BY_USE`: they follow the land-use colours of US planning maps (residential yellow, retail red, office a lighter red, civic blue, industrial purple, parking grey). `massSegmentColorCommand` gives the `replace-mass` command that sets the colour of one part, or takes it away with `undefined`. The 3D model and the plan draw the part in that colour.

```typescript theme={null}
import { massSegmentColorCommand } from '@opengeometry/floorkit';

export function paintTower(session: BuildingSession, color: string | undefined): void {
  const command = massSegmentColorCommand(session.snapshot(), 'building', 'tower', color);
  if (command) session.apply(command, session.revision);
}
```

To use other colours by use, give your own table as the build option `massColors` where the project is opened: `ProjectManager.open(text, myColours)`. The 3D model, the plans and the sheets of that project then use it. It replaces the whole table; a use that it does not list takes `#a0b8c8`.

```typescript theme={null}
import type { ProjectManagerOptions } from '@opengeometry/floorkit';

export const myColours: ProjectManagerOptions = {
  build: { massColors: { residential: '#f2d675', office: '#e58f7b', park: '#7fb069' } },
};
```

**Footprint and storeys.** To change a footprint, or to add and remove storeys, send `replace-mass` again with the new mass and the new storey list, as in `setSiteAndMass`.

**A new building.** `massBuildingCommand` makes the `add-building` command from an outline in site coordinates. It checks the outline, stacks the storeys from the site grade and gives ids that are new in the whole model.

```typescript theme={null}
import { massBuildingCommand } from '@opengeometry/floorkit';

export function addBuildingFromOutline(session: BuildingSession): void {
  const command = massBuildingCommand(
    session.snapshot(),
    [
      [16, 0],
      [24, 0],
      [24, 8],
      [16, 8],
    ],
    { storeyHeight: 3, storeyCount: 4, use: 'residential' },
  );
  session.apply(command, session.revision);
}
```

## Edit the mass in 2D

A plan view does not draw the massing unless you ask: the switch `showMassing` is off by default, so existing plans and sheets do not change. Turn it on with `replace-project-item` on the views of a `ProjectManager` (one undo step), then mount the plan editor for that view.

```typescript theme={null}
import type { ManagedPlanEditor, ProjectManager } from '@opengeometry/floorkit';

export async function editMassInPlan(
  manager: ProjectManager,
  container: HTMLElement,
  viewId: string,
): Promise<ManagedPlanEditor | undefined> {
  const view = manager.snapshot().views.find((item) => item.id === viewId);
  if (view?.kind !== 'plan') return undefined;
  manager.apply({
    kind: 'replace-project-item',
    list: 'views',
    id: view.id,
    item: { ...view, showMassing: true },
  });
  const plan = await manager.mountPlanEditor(container, viewId);
  if (plan.renderer) plan.renderer.onDrag = (change) => console.log('live', change.kind);
  return plan;
}
```

The plan then draws every building's mass with its parts, the parcel (a continuous line) and the setback line (dashed): a mass inside the dashed line passes the setback check of `evaluateZoning`. The parcel and the setbacks are only drawn; a click passes through them. The user clicks a part, drags a corner or edge dot to reshape it, drags the part to move it, drags the round grip to turn it, and presses Delete to remove it. Each gesture is one `replace-mass` and one undo step, as in 3D. The plan has no height grip: change the height in the 3D editor, or type it with `massSegmentHeightCommand`. `onDrag` is on both editors (`GUIEditor.onDrag` in 3D), so you can show live numbers while a drag is in progress. Sheets and PDFs show what the plan shows.

**Draw it in the plan.** `plan.startMassDrawing({ storeyHeight: 3, storeyCount: 4 })` lets the user click the corners of the outline: a click on the first corner or Enter closes it, and Escape cancels. It saves the same `add-building` as one undo step and resolves with the id of the new mass, or with `undefined` if the user cancels. `startOutlineDrawing` returns the outline alone, as site XZ points, and saves nothing; an app can pass that polygon to `massBuildingCommand` or use it for something else.

```typescript theme={null}
export async function drawBuilding(plan: ManagedPlanEditor): Promise<string | undefined> {
  return plan.startMassDrawing({ storeyHeight: 3, storeyCount: 4, use: 'residential' });
}
```

See the [`ManagedPlanEditor` API](/floorkit/api/editor/managed-plan-editor) and [Add a plan editor](/floorkit/guides/add-a-plan-editor#edit-a-mass).

## Check the zoning rules

`evaluateZoning` is pure. It does not read the session, so you run it again after each step. Give it the lot, the masses and the rules.

```typescript theme={null}
import { evaluateZoning, type ZoningReport } from '@opengeometry/floorkit';

export function checkZoning(session: BuildingSession): ZoningReport | undefined {
  const { site, buildings } = session.snapshot();
  if (!site?.boundary || !site.zoning) return undefined;
  const masses = buildings.flatMap((building) =>
    building.mass ? [irMassToMassLike(building)] : [],
  );
  const lot = { profile: site.boundary, lotLines: site.lotLines, elevation: site.elevation };
  return evaluateZoning(lot, masses, site.zoning);
}
```

With the rules above the mass is within the limit at first (192 m² on a lot of 612 m² is a ratio of 0.31). When you raise the tower to 9 m, the ratio is 0.39 and the report names `exceeds-far`. When you make the ground storey a parking storey, it counts 144 m² and the ratio is 0.24.

To change a rule, send `replace-site` with the new lot. It replaces the boundary, the grade and the zoning together. The podium is 4 m from the lot line, so a setback of 5 m puts it in violation:

```typescript theme={null}
export function setSetback(session: BuildingSession, metres: number): void {
  const site = session.snapshot().site;
  if (!site?.zoning) throw new Error('The model has no site with zoning rules.');
  const rules = site.zoning.rules.map((rule) =>
    rule.kind === 'envelope-plane' ? { ...rule, offsetAtGrade: metres } : rule,
  );
  session.apply(
    { kind: 'replace-site', site: { ...site, zoning: { ...site.zoning, rules } } },
    session.revision,
  );
}
```

Call `session.undo(session.revision)` to go back one step. See [Undo and redo](/floorkit/guides/undo-and-redo).

## Edit the mass in 3D

With a 3D editor bound to the session (see [Add a 3D editor](/floorkit/guides/add-a-3d-editor)), the user edits the mass with the pointer. A click selects a part, the podium or the tower. The push/pull grip changes its height, the move and turn grips move or turn it, and Delete removes it. Each gesture saves one command and one undo step. `GUIEditor.onDrag` gives the live target before the save, so that you can show live numbers such as the floor area. See the [`GUIEditor` API](/floorkit/api/editor/gui-editor) and [`massSegmentHeightCommand`](/floorkit/api/architecture/massing-helpers).

## Turn the mass into walls, slabs and roofs

When the volume is correct, `massToShell` makes a building with real elements from it. Each storey gets an outer wall. Each part gets floor slabs and a flat roof.

```typescript theme={null}
export function massToElements(model: BuildingModel): void {
  const building = model.buildings[0];
  if (!building) throw new Error('The model has no building.');
  model.buildings[0] = massToShell(irMassToMassLike(building), { wallThickness: 0.3 });
}
```

Pass `massToElements` to `replaceBuilding`. This opens a new session, and the undo history ends. The new building has no mass, and its id is the id of the mass plus `-shell`: here `building-mass-shell`. The storeys keep their ids. Use the new building id in your edit commands.

## Rules

* A footprint is a list of `[x, z]` points in metres. A hole in a footprint is a courtyard.
* The parts stack from the bottom to the top. The first part starts at the `elevation` of the mass.
* The `height` of a part must be the sum of the heights of its storeys. If it is not, the model is not valid.
* A storey belongs to one part only.
* `use` can be `residential`, `office`, `retail`, `hotel`, `civic`, `industrial`, `parking`, `mixed` or `other`. A different word gives a warning.
* The storeys of a mass can be empty. They do not need walls.
* A colour is a `#rrggbb` string. A command with another colour is refused with `invalid-command`.
* A plan view shows the mass when the view has `showMassing: true`; the plan editor then edits it. See [Add a plan editor](/floorkit/guides/add-a-plan-editor#edit-a-mass).
* Sections and elevations exclude masses. Turn the mass into a shell when you need physical cuts.

## Next

<CardGroup cols={2}>
  <Card title="Define a room" icon="draw-polygon" href="/floorkit/guides/define-a-room">
    Give names and areas to the spaces in a storey.
  </Card>

  <Card title="Building API" icon="code" href="/floorkit/api/architecture/building">
    See each option and each quantity of a mass.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.