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

# Takeoff parts and breakdown

> The stored shape of Multi-line and Multi-segment measurements, and the grouping rules that produce quantity_breakdown and per-tag subtotals

`estimate_takeoff_measurements` rows with `type` `multi_line` or `multi_segment` carry two server-side fields beyond the flat `points` list: `parts` (client-written structure) and `quantity_breakdown` (server-written grouping). The shapes are defined in `@stralos-co-nz/base` (`packages/base/src/takeoffParts.ts`). The calculator in `@stralos-co-nz/calculators` (`packages/calculators/src/takeoffParts.ts`) provides the one `takeoffBreakdown` function both api-estimate and feature-estimate must run, so the live quantity readout and the stored figure cannot drift.

> [!NOTE]
>
> `Line`, `Area`, and `Count` measurements carry neither `parts` nor `quantity_breakdown`. Both fields are `NULL` for those types.

## Why `parts` exists

`points` is a flat list for every measurement type. A Multi-line's lines are not joined — the gap from the last point of line *k* to the first point of line *k+1* is never measured. Without `parts`, a reader cannot tell where one line ends and the next begins and would measure those gaps as spans. `parts` records the structure (line starts for a Multi-line, one tag per span for a Multi-segment) so that rule is enforced by data, not by the reader's assumptions.

## `TakeoffParts`

`parts` is one of two shapes:

```ts theme={null}
import type { TakeoffParts, TakeoffLinesParts, TakeoffSpansParts } from "@stralos-co-nz/base";
```

### `TakeoffLinesParts` (Multi-line)

```ts theme={null}
type TakeoffLinesParts = {
  v: 1;
  kind: "lines";
  starts: number[];       // point indexes where each line begins; starts[0] === 0 always
  tags?: (string | null)[]; // 1.8.0 — one tag id per line, or null for untagged; absent on untagged rows
  tagMeta?: Record<string, TakeoffTagMeta>; // 1.8.0 — present exactly with tags
};
```

`starts[i]` is the index in `points` where line `i` begins. A Multi-line with `n` lines has `starts.length === n`, with `starts[0] === 0` always.

**Tagging (base 1.8.0)**: `tags[i]` is the UUID of the takeoff tag applied to line `i`, or `null` for an untagged line. `tags.length` must equal `starts.length`. A row stored without `tags` is an untagged Multi-line and is backward-compatible with all earlier readers.

### `TakeoffSpansParts` (Multi-segment)

```ts theme={null}
type TakeoffSpansParts = {
  v: 1;
  kind: "spans";
  tags: (string | null)[]; // one tag id per span (points[i] → points[i+1]), or null
  tagMeta: Record<string, TakeoffTagMeta>; // always present
};
```

`tags[i]` applies to the span from `points[i]` to `points[i + 1]`. `tags.length` must equal `points.length − 1`.

### `TakeoffTagMeta`

```ts theme={null}
type TakeoffTagMeta = { label: string; colour: string };
```

A snapshot of the tag's label and colour at the time the measurement was last saved. Tag labels are at most 60 characters (`TAKEOFF_TAG_LABEL_MAX`). Colour is `#rrggbb`.

`tagMeta` is keyed by the non-null ids in `tags`. Every id in `tags` that is non-null must appear in `tagMeta`; ids with no span or line in `tags` must not appear.

## `TakeoffBreakdown`

`quantity_breakdown` is server-written and must never be accepted from a client. It is one of two shapes:

### `TakeoffLinesBreakdown`

```ts theme={null}
type TakeoffLinesBreakdown = {
  v: 1;
  kind: "lines";
  lineLengths: number[]; // 2 dp, one per line in line order
  byTag?: TakeoffTagSubtotal[]; // 1.8.0 — present exactly when parts.tags is present
};
```

### `TakeoffSpansBreakdown`

```ts theme={null}
type TakeoffSpansBreakdown = {
  v: 1;
  kind: "spans";
  spanLengths: number[]; // 3 dp, one per span in span order
  byTag: TakeoffTagSubtotal[]; // always present
};
```

### `TakeoffTagSubtotal`

```ts theme={null}
type TakeoffTagSubtotal = {
  tag: string | null;  // null for the untagged bucket
  quantity: number;    // 2 dp
  spans: number;       // count of spans carrying this tag (or measured spans inside tagged lines)
  lines?: number;      // 1.8.0 — Multi-line only: count of lines carrying this tag
};
```

`byTag` is in order of each tag's first span or line. Ids are lower-cased. `tag: null` is the untagged bucket.

`quantity` is computed as `round2(Σ its 3 dp span lengths)` for Multi-segment, and as `Σ of its lines' 2 dp lineLengths` for Multi-line (calculators 1.10.0). The measurement's `quantity` is `Σ of the 2 dp subtotals` — never `round2(Σ all spans)` — so the breakdown column always adds up to the stored quantity.

## Validation

`takeoffPartsIssues(parts, type, pointCount)` returns a `TakeoffPartsIssue[]`. An empty array means the value is acceptable. Both api-estimate and feature-estimate run it on every write, including `Line`, `Area`, and `Count` writes (which must have `null` parts).

| Code | Condition |
| - | - |
| `type_mismatch` | `parts` is present on a type that takes none, absent on one that requires it, or the wrong kind. |
| `version` | `v` is not `1`. |
| `kind` | `kind` is not `"lines"` or `"spans"`. |
| `points` | `pointCount` is outside 2–2000 (`TAKEOFF_MAX_POINTS`). |
| `starts` | `starts` is not integers from 0, strictly increasing, within `points`. |
| `line_too_short` | A line has fewer than 2 points. |
| `lines_too_many` | `starts.length` exceeds 200 (`TAKEOFF_MAX_LINES`). |
| `tags_length` | `tags.length` is not `starts.length` (lines) or `pointCount − 1` (spans). |
| `tag_id` | A tag entry is neither a UUID nor `null`. |
| `tag_meta` | `tagMeta` is missing beside `tags`, names an id no span or line carries, or an entry's label or colour is invalid. |

## Grouping rules

`takeoffBreakdown(parts, pointCount, spanLengths)` is the one function hosts run to produce `quantity_breakdown` and `quantity`. Its three invariants:

1. **A line's gap is never measured.** A Multi-line's spans are the point pairs *inside* each line. The step from the last point of line *k* to the first of line *k+1* is not a span. Use `takeoffSpanEndpoints` to enumerate which pairs to measure.
2. **Group by tag id, not by position.** Two runs of the same tag on a Multi-segment produce one subtotal. Tag ids are compared lower-cased.
3. **(1.10.0) A tagged Multi-line groups whole lines by tag.** A line's tag covers its whole length. A tag's subtotal is the sum of its lines' already-rounded 2 dp lengths. Tagging does not change the measurement's total quantity.

## Point edits

`takeoffPartsAfterInsert(parts, pointCount, index)` returns the updated `parts` after a midpoint handle inserts a new point at `index`. For a Multi-segment, the new point splits its span and both halves inherit that span's tag. For a Multi-line, the insert must fall strictly inside one line and never changes the line count or its tag.

`takeoffPartsAfterDelete(parts, pointCount, index)` returns the updated `parts` after a point is deleted, or `undefined` if the delete would leave a line or the whole run with fewer than 2 points. For a Multi-segment interior delete, the merged span takes the left (incoming) span's tag. For Multi-line, every line keeps its tag as long as the line survives.

## Removed sources

`takeoffRemovedSources(before, after)` returns the link sources an edit removes. Hosts use this to decide whether to show a confirm-and-unlink prompt.

From calculators 1.16.0, no edit of a measurement that still exists removes its total:

* Deleting a Multi-segment span (an end delete, or an interior delete that merges two spans) or a whole Multi-line **line** is a geometry change. The total and every surviving tag's subtotal recalculate through their links.
* A tag is removed only when no span or line carries it any more, because its last span or line was deleted or retagged.
* Drags, inserts, and retagging a non-last span or line remove nothing.
* Untagged spans and lines are never a source, so an untagged Multi-line lists no tags.

`TakeoffRemovedSources.total` is therefore always `false`. The field stays in the type so hosts that read `removed.total` still compile; it is due for removal at the next major version.

See [Linked work-item quantity](https://help.stralos.app/internal/quantity-link.md) for how per-tag subtotals feed `QuantityLinkV1`.

## Constants

| Export | Value | Notes |
| - | - | - |
| `TAKEOFF_PARTS_VERSION` | `1` | The only accepted version. |
| `TAKEOFF_MAX_POINTS` | `2000` | Point cap per measurement. |
| `TAKEOFF_MAX_LINES` | `200` | Line cap per Multi-line. |
| `TAKEOFF_TAG_LABEL_MAX` | `60` | Maximum tag label length in characters. |
| `TAKEOFF_SPAN_LENGTH_SCALE` | `3` | Decimal places for individual span lengths. |
| `TAKEOFF_SUBTOTAL_SCALE` | `2` | Decimal places for line lengths, tag subtotals, and the measurement quantity. |


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