# StrokeModel

Import it from `@grafloria/engine`.

Also has every member of `DiagramEntity`, listed on its own entry.

A freehand ink stroke: an ordered point list, a style, and an identity.

A first-class `DiagramEntity`, which is not a formality — it is what makes every
other gate in this wave close for free:
  • `trackChange()` → `markDirty()` → the GLOBAL MUTATION EPOCH, which is the thing
    both frame gates read (`svg-renderer.ts:795`, `create-diagram.ts:369`). A stroke
    that were a plain object would mutate the picture without moving the epoch, and
    both gates would skip the frame: you would draw and see nothing.
  • `on('change')` → `OpCapture` picks it up through the same funnel as everything
    else. No parallel bookkeeping to drift out of step.
  • `serialize()`/`dispose()`/`version` → the document contracts.

```ts
class StrokeModel extends DiagramEntity
```

**Methods**

- `constructor( points: readonly StrokePoint[] = [], style: StrokeStyle = DEFAULT_STROKE_STYLE, options: { id?: string; uuid?: string; label?: string } = {} )`
- `getPoints(): readonly StrokePoint[]`
- `setPoints(points: readonly StrokePoint[]): void` — Replace the geometry.

Emits `trackChange('points', …)` like any other register. NOTE that capture used
to drop EVERY `points` change on the floor (`DERIVED = new Set(['points'])`),
because on a LINK `points` is routed geometry that each peer recomputes for
itself. On a stroke `points` is the authored content itself. That set is now
scoped per target — see `collab/capture.ts`. Without that fix, editing a stroke's
geometry would silently fail to reach any other peer.
- `get pointCount(): number`
- `getBounds(): Rectangle` — Axis-aligned bounds, INFLATED by the stroke's own half-width (ink has girth).
- `hitTest(x: number, y: number, tolerance = 0): boolean` — Is `(x, y)` on the ink, within `tolerance` world units of it?
- `intersectsSegment(a: Point, b: Point, tolerance = 0): boolean` — Did a pointer travelling `a`→`b` sweep across this ink? THE ERASER'S QUESTION. Segment-vs-segment, not point-vs-segment — see {@link segmentDistance}.
- `getStyle(): Readonly<StrokeStyle>`
- `setStyle(style: Partial<StrokeStyle>): void`
- `replaceStyle(style: Partial<StrokeStyle>): void` — REPLACE the whole style object — the write `setStyle` cannot express, and the collab
reducer's write path.

`setStyle` MERGES, so applying a `style` op with it meant a peer could gain a key and
never lose one: the author clears an `opacity` and every other peer keeps the faded
stroke forever. `NodeModel.replaceStyle` is the same seam for the same reason, and a
plain field assignment is not a substitute — it would skip the `bounds` invalidation
below and leave the peer measuring the stroke at its old width.
- `override getLabel(): string | undefined` — The accessible name, when the author gave one. See the a11y note on the
ink layer. Overrides the DiagramEntity canon (metadata.label): a stroke's
label lives in its own serialized `label` property, not the metadata bag.
- `override setLabel(label: string | undefined): void` — CANONICAL label write. Writes `metadata.label` (tracked → dirty → repaint)
and mirrors into the legacy `data['label']` slot when the entity has a
data bag, so any reader still on the old field sees the same value. The
mirror is a shadow: metadata is authoritative on every read path.
- `serialize(): SerializedStroke` — Serialize entity to JSON
- `static fromJSON(data: SerializedStroke): StrokeModel` (static)
- `static fromRawPoints( raw: readonly StrokePoint[], style: StrokeStyle = DEFAULT_STROKE_STYLE, options: { id?: string; label?: string; epsilon?: number } = {} ): StrokeModel` (static) — Build a stroke from a RAW POINTER TRACE. This is the only constructor the draw tool
uses, and the simplification is not optional decoration.

A two-second scribble at 120Hz is ~240 samples, most of them a fraction of a pixel
apart. Persisting them all means a document that is mostly float noise, an op
payload that is kilobytes per line, and an SVG path the browser re-parses on every
frame. The brief's phrasing is exactly right: a 500-point stroke that serialises as
500 points is a bug.

Typical reduction on real ink is 85-95%.

IT PRESERVES PRESSURE, and that is a property of the algorithm rather than luck:
Douglas-Peucker SELECTS a subset of the input points (it returns the very objects it
was given — `PathSimplifier.ts:123`), it never interpolates new ones. So the
`pressure` riding on each retained sample rides through untouched. If it ever grows
an interpolating mode this breaks silently, so the spec pins it.
