Import it from @grafloria/engine.
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.
tsclass 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(): numbergetBounds(): 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, withintoleranceworld units of it?intersectsSegment(a: Point, b: Point, tolerance = 0): boolean— Did a pointer travellinga→bsweep across this ink? THE ERASER'S QUESTION. Segment-vs-segment, not point-vs-segment — see {@link segmentDistance}.getStyle(): Readonly<StrokeStyle>setStyle(style: Partial<StrokeStyle>): voidreplaceStyle(style: Partial<StrokeStyle>): void— REPLACE the whole style object — the writesetStylecannot 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 serializedlabelproperty, not the metadata bag.override setLabel(label: string | undefined): void— CANONICAL label write. Writesmetadata.label(tracked → dirty → repaint) and mirrors into the legacydata['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 JSONstatic 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.
Was this page helpful?