@weasel-js/stroke 0.0.0-stage → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 orochi235
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,47 @@
1
- # Temporary Holding Version
1
+ # @weasel-js/stroke
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A stroke as geometry: the mesh a renderer draws for it, the marker heads that
4
+ cap it, and the region its ink covers as a path.
5
+
6
+ Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
7
+ scene-graph canvas kit for React. See the
8
+ [API reference](https://orochi235.github.io/weasel/api/).
9
+
10
+ ```sh
11
+ npm install @weasel-js/stroke
12
+ ```
13
+
14
+ A path comes from `@weasel-js/geom` and the `Stroke` describing how to draw
15
+ its line from `@weasel-js/paint`. This package turns the pair into triangles or
16
+ into another path. It has no scene graph, no renderer, and no React.
17
+
18
+ ## The ribbon
19
+
20
+ `tessellateStroke(path, stroke, options)` answers a triangle mesh: width, cap,
21
+ join, miter limit, dash, alignment, and per-vertex widths are all in it.
22
+ `resolveStrokeWidth` turns a `{ px }` width into path units at a given scale.
23
+
24
+ A stroke that must stay a fixed number of screen pixels wide under a
25
+ non-uniform zoom is built in a *stroke metric*, the linear part of the
26
+ transform it will be drawn through. `strokeSpaceOf` makes one, and the
27
+ `polylineIntoMetric` / `coordsOutOfMetric` family moves points in and out of
28
+ it.
29
+
30
+ ## Markers
31
+
32
+ A marker is a shape drawn at a stroke's start, end, or interior vertices: an
33
+ arrowhead, a dot, a bar. `registerMarker` adds one under a key, and a `Stroke`
34
+ names it in `markerStart`, `markerMid`, or `markerEnd`. `BUILTIN_MARKERS` lists
35
+ the ones registered from the start.
36
+
37
+ `markerSites` finds where markers sit along a polyline and at what angle.
38
+ `markerHeads` places each registered shape on its site and answers the
39
+ geometry, unpainted. `markerInset` and `strokeInsets` say how far to pull the
40
+ line back so it ends inside the head, and `markerReach` how far a stroke's
41
+ heads can paint from the vertex they sit on.
42
+
43
+ ## A stroke as a path
44
+
45
+ `strokeToPath(path, stroke)` answers the outline of everything above as one
46
+ fillable path: the ribbon and its heads, unioned. Curves are flattened, so the
47
+ result holds only straight segments.
@@ -0,0 +1,108 @@
1
+ import { PATH_M, PATH_L, PATH_Z } from '@weasel-js/geom';
2
+ import { createReflectable } from '@weasel-js/registry';
3
+
4
+ // src/strokeMarkerShapes.ts
5
+ function poly(pts, size, close = true) {
6
+ const n = pts.length / 2;
7
+ const commands = new Uint8Array(close ? n + 1 : n);
8
+ commands[0] = PATH_M;
9
+ for (let i = 1; i < n; i++) commands[i] = PATH_L;
10
+ if (close) commands[n] = PATH_Z;
11
+ const coords = new Float32Array(pts.length);
12
+ for (let i = 0; i < pts.length; i++) coords[i] = pts[i] * size;
13
+ return { kind: "polygon", commands, coords, fillRule: "nonzero" };
14
+ }
15
+ function circle(cx, r, size, segments = 32) {
16
+ const pts = [];
17
+ for (let i = 0; i < segments; i++) {
18
+ const theta = i / segments * Math.PI * 2;
19
+ pts.push(cx + r * Math.cos(theta), r * Math.sin(theta));
20
+ }
21
+ return poly(pts, size);
22
+ }
23
+ var BUILTIN_MARKERS = [
24
+ {
25
+ id: "arrow",
26
+ reads: [],
27
+ inset: 3,
28
+ path: ({ size }) => poly([0, 0, -3, -1.5, -3, 1.5], size)
29
+ },
30
+ {
31
+ id: "arrow-open",
32
+ reads: [],
33
+ inset: 0,
34
+ fill: "none",
35
+ outline: { width: 1 },
36
+ // Open at the back, so the ribbon runs to the vertex and the arms meet it.
37
+ // Arms 3.5 long at ±35°: the back opening is 4 units against 2 units of
38
+ // arm, so the V still reads open. Shorter or narrower and the two arms
39
+ // close the gap themselves and it paints as a solid triangle.
40
+ path: ({ size }) => poly([-2.867, -2.007, 0, 0, -2.867, 2.007], size, false)
41
+ },
42
+ {
43
+ id: "arrow-concave",
44
+ reads: [],
45
+ inset: 3,
46
+ path: ({ size }) => poly([0, 0, -3, -1.5, -2, 0, -3, 1.5], size)
47
+ },
48
+ {
49
+ id: "diamond",
50
+ reads: [],
51
+ inset: 4,
52
+ path: ({ size }) => poly([0, 0, -2, -1.2, -4, 0, -2, 1.2], size)
53
+ },
54
+ {
55
+ id: "diamond-hollow",
56
+ reads: [],
57
+ inset: 4,
58
+ fill: "none",
59
+ outline: { width: 0.5 },
60
+ path: ({ size }) => poly([0, 0, -2, -1.2, -4, 0, -2, 1.2], size)
61
+ },
62
+ {
63
+ id: "circle",
64
+ reads: [],
65
+ inset: 2,
66
+ path: ({ size }) => circle(-1, 1, size)
67
+ },
68
+ {
69
+ id: "square",
70
+ reads: [],
71
+ inset: 2,
72
+ path: ({ size }) => poly([0, -1, 0, 1, -2, 1, -2, -1], size)
73
+ },
74
+ {
75
+ id: "bar",
76
+ reads: [],
77
+ inset: 0,
78
+ fill: "none",
79
+ outline: { width: 1 },
80
+ path: ({ size }) => poly([0, -1.5, 0, 1.5], size, false)
81
+ }
82
+ ];
83
+ var MARKERS = createReflectable();
84
+ function seedBuiltins() {
85
+ for (const entry of BUILTIN_MARKERS) MARKERS.push(entry.id, entry, { source: "kit" });
86
+ }
87
+ seedBuiltins();
88
+ var markerRegistry = MARKERS.reflection;
89
+ function markerGeneration() {
90
+ return MARKERS.getVersion();
91
+ }
92
+ function registerMarker(entry) {
93
+ return MARKERS.push(entry.id, entry);
94
+ }
95
+ function getMarker(key) {
96
+ return key === void 0 ? void 0 : MARKERS.get(key);
97
+ }
98
+ function listMarkers() {
99
+ return MARKERS.entries().map((e) => e.value);
100
+ }
101
+ function _resetMarkersForTests() {
102
+ MARKERS.clear();
103
+ seedBuiltins();
104
+ }
105
+
106
+ export { BUILTIN_MARKERS, _resetMarkersForTests, getMarker, listMarkers, markerGeneration, markerRegistry, registerMarker };
107
+ //# sourceMappingURL=chunk-FQB7N6YD.js.map
108
+ //# sourceMappingURL=chunk-FQB7N6YD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/strokeMarkerShapes.ts","../src/strokeMarkers.ts"],"names":[],"mappings":"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","file":"chunk-FQB7N6YD.js","sourcesContent":["/**\n * The kit's built-in stroke-marker vocabulary.\n *\n * Every entry is authored with its anchor at the origin and pointing +X, so\n * the line arrives from -X and no geometry sits at positive X. Coordinates are\n * in units of `MarkerCtx.size`, which defaults to the resolved stroke width —\n * one definition is therefore correct at any line weight.\n *\n * Not to be confused with `features/paths/markers.ts`, which builds\n * decorative chrome shapes and is unrelated.\n */\n\nimport { PATH_M, PATH_L, PATH_Z, type PolygonPath } from '@weasel-js/geom';\nimport type { MarkerCtx, MarkerEntry } from './strokeMarkers';\n\nfunction poly(pts: readonly number[], size: number, close = true): PolygonPath {\n const n = pts.length / 2;\n const commands = new Uint8Array(close ? n + 1 : n);\n commands[0] = PATH_M;\n for (let i = 1; i < n; i++) commands[i] = PATH_L;\n if (close) commands[n] = PATH_Z;\n const coords = new Float32Array(pts.length);\n for (let i = 0; i < pts.length; i++) coords[i] = pts[i] * size;\n return { kind: 'polygon', commands, coords, fillRule: 'nonzero' };\n}\n\n/** Circle of radius `r` centred at `cx`, in marker units. */\nfunction circle(cx: number, r: number, size: number, segments = 32): PolygonPath {\n const pts: number[] = [];\n for (let i = 0; i < segments; i++) {\n const theta = (i / segments) * Math.PI * 2;\n pts.push(cx + r * Math.cos(theta), r * Math.sin(theta));\n }\n return poly(pts, size);\n}\n\n/** The markers the registry is seeded with: `arrow`, `arrow-open`,\n * `arrow-concave`, `diamond`, `diamond-hollow`, `circle`, `square` and `bar`. */\nexport const BUILTIN_MARKERS: readonly MarkerEntry[] = [\n {\n id: 'arrow',\n reads: [],\n inset: 3,\n path: ({ size }: MarkerCtx) => poly([0, 0, -3, -1.5, -3, 1.5], size),\n },\n {\n id: 'arrow-open',\n reads: [],\n inset: 0,\n fill: 'none',\n outline: { width: 1 },\n // Open at the back, so the ribbon runs to the vertex and the arms meet it.\n // Arms 3.5 long at ±35°: the back opening is 4 units against 2 units of\n // arm, so the V still reads open. Shorter or narrower and the two arms\n // close the gap themselves and it paints as a solid triangle.\n path: ({ size }: MarkerCtx) =>\n poly([-2.867, -2.007, 0, 0, -2.867, 2.007], size, false),\n },\n {\n id: 'arrow-concave',\n reads: [],\n inset: 3,\n path: ({ size }: MarkerCtx) => poly([0, 0, -3, -1.5, -2, 0, -3, 1.5], size),\n },\n {\n id: 'diamond',\n reads: [],\n inset: 4,\n path: ({ size }: MarkerCtx) => poly([0, 0, -2, -1.2, -4, 0, -2, 1.2], size),\n },\n {\n id: 'diamond-hollow',\n reads: [],\n inset: 4,\n fill: 'none',\n outline: { width: 0.5 },\n path: ({ size }: MarkerCtx) => poly([0, 0, -2, -1.2, -4, 0, -2, 1.2], size),\n },\n {\n id: 'circle',\n reads: [],\n inset: 2,\n path: ({ size }: MarkerCtx) => circle(-1, 1, size),\n },\n {\n id: 'square',\n reads: [],\n inset: 2,\n path: ({ size }: MarkerCtx) => poly([0, -1, 0, 1, -2, 1, -2, -1], size),\n },\n {\n id: 'bar',\n reads: [],\n inset: 0,\n fill: 'none',\n outline: { width: 1 },\n path: ({ size }: MarkerCtx) => poly([0, -1.5, 0, 1.5], size, false),\n },\n];\n","/**\n * The stroke-marker registry — arrowheads and other line terminators.\n *\n * Keyed by string, because a key is what sits in `stroke.markerEnd`. That is\n * the difference from `registerNodeShape`, which resolves by first-matching\n * predicate and whose `id` is only a label.\n *\n * A built-in's SVG slot lives in `@weasel-js/svg` and cannot be imported here\n * without inverting a package dependency, so an entry carries `toSvg` for that\n * package to consume — the same split `PaintKindEntry` uses.\n */\n\nimport type { Paint, MarkerKey, Stroke } from '@weasel-js/paint';\nimport type { Path } from '@weasel-js/geom';\nimport { createReflectable, type Reflection } from '@weasel-js/registry';\nimport { BUILTIN_MARKERS } from './strokeMarkerShapes';\n\n/** What an entry's `path` is given. Entries that ignore it may return a\n * constant path. */\nexport interface MarkerCtx {\n /** One geometry unit, in the same world units the ribbon is tessellated in.\n * Defaults to the resolved stroke width. */\n readonly size: number;\n /** The stroke this marker belongs to, already width-resolved. */\n readonly stroke: Stroke;\n}\n\n/** `'line'` means the stroke's own paint — SVG 2's `context-stroke`, as the\n * default rather than an opt-in. */\nexport type MarkerPaint = Paint | 'line' | 'none';\n\n/** A stroke-marker registry entry: a terminator's geometry, paint and inset. */\nexport interface MarkerEntry {\n /** `'kit:'`-free for built-ins so the key matches the SVG attribute value;\n * consumers should prefix (`'app-my-head'`). A key becomes the `id` of the\n * emitted `<marker>` def, so it must be a valid XML name — no colons. */\n id: MarkerKey;\n /** Geometry with its anchor at the origin, pointing +X, in units of\n * `ctx.size`. An arbitrary anchor is expressed by where the geometry is\n * drawn, which is why there is no `refX`/`refY`. */\n path(ctx: MarkerCtx): Path;\n /** Default `'line'`. */\n fill?: MarkerPaint;\n /** Outline width is in the same units as `path`. `false` (the default)\n * means no outline. */\n outline?: { width: number; paint?: MarkerPaint } | false;\n /** How far back along the line the stroke stops, in units of `ctx.size`.\n * Default 0. A property of the shape, not a setting on the stroke: an open\n * V needs 0 or its arms stop meeting the line. */\n inset?: number;\n /** `'auto'` (default) follows the line; a number is a fixed angle in\n * radians, ignoring the line — SVG's `orient=\"<angle>\"`. */\n orient?: 'auto' | number;\n /** The stroke fields `path` reads besides `ctx.size` — `[]` for a shape\n * drawn from size alone. Declared, a head is reused across stroke objects\n * whose listed fields are equal; omitted, only across frames that pass the\n * same stroke object, since `path` might read any of it. */\n reads?: readonly (keyof Stroke)[];\n /** Emits the `<marker>` def. Consumed by `@weasel-js/svg`. */\n toSvg?(id: string, entry: MarkerEntry): string;\n}\n\nconst MARKERS = createReflectable<MarkerEntry>();\n\nfunction seedBuiltins(): void {\n for (const entry of BUILTIN_MARKERS) MARKERS.push(entry.id, entry, { source: 'kit' });\n}\nseedBuiltins();\n\n/** Every registered marker, with the overrides each displaced. */\nexport const markerRegistry: Reflection<MarkerEntry> = MARKERS.reflection;\n\n/** Bumped whenever the registered set changes, for caches of marker geometry. */\nexport function markerGeneration(): number {\n return MARKERS.getVersion();\n}\n\n/** Register a marker. Returns a disposer. Re-registering an existing id is an\n * override; disposing it uncovers whatever it displaced rather than deleting the key. */\nexport function registerMarker(entry: MarkerEntry): () => void {\n return MARKERS.push(entry.id, entry);\n}\n\n/** The entry for `key`, or `undefined`. */\nexport function getMarker(key: string | undefined): MarkerEntry | undefined {\n return key === undefined ? undefined : MARKERS.get(key);\n}\n\n/** Every registered marker, built-ins first, in registration order. */\nexport function listMarkers(): readonly MarkerEntry[] {\n return MARKERS.entries().map((e) => e.value);\n}\n\n/** Test helper. Do not call from product code. */\nexport function _resetMarkersForTests(): void {\n MARKERS.clear();\n seedBuiltins();\n}\n"]}
@@ -0,0 +1,331 @@
1
+ import { Path, PolygonPath, Mat3 } from '@weasel-js/geom';
2
+ import { ScreenLength, Stroke, MarkerRef } from '@weasel-js/paint';
3
+ import { Polyline, Mesh } from '@weasel-js/geom/tessellate';
4
+ import { M as MarkerEntry } from './test-seams-D-zMT-bn.js';
5
+ export { a as MarkerCtx, b as MarkerPaint, g as getMarker, l as listMarkers, m as markerGeneration, c as markerRegistry, r as registerMarker } from './test-seams-D-zMT-bn.js';
6
+ import '@weasel-js/registry';
7
+
8
+ /**
9
+ * The space a screen-pixel stroke is built in.
10
+ *
11
+ * A `{ px }` stroke is exact on screen only when its ribbon is offset in a
12
+ * space that measures lengths the way the screen does. The transform's linear
13
+ * part is `scale · R · P`: a uniform scale, a rotation (or reflection) that
14
+ * moves no lengths, and `P`, the symmetric unit-determinant stretch left over.
15
+ * Offsetting the ribbon after `P` and mapping it back through `P⁻¹` gives a
16
+ * world-space mesh that the full transform turns into a ribbon of exactly
17
+ * `width · scale` pixels in every direction — and keeps the mesh drawable by
18
+ * every path that draws a world-space ribbon.
19
+ *
20
+ * `P` depends only on the lengths the transform induces (`LᵀL`), not on any
21
+ * rotation applied after it, so a view that only pans or zooms uniformly keeps
22
+ * the same metric and the ribbon cache keeps hitting.
23
+ */
24
+
25
+ /** `P` = [[p, q], [q, r]], with `pr − q² = 1`. */
26
+ interface StrokeMetric {
27
+ readonly p: number;
28
+ readonly q: number;
29
+ readonly r: number;
30
+ }
31
+ /** A space a screen-sized part of a stroke is built in: the metric, and how
32
+ * many screen pixels one of its units is. */
33
+ interface ScreenSpace {
34
+ readonly metric: StrokeMetric;
35
+ readonly pxPerUnit: number;
36
+ }
37
+ /**
38
+ * Where each part of a stroke is built. An absent part is built in world:
39
+ * everything is under a similarity, and otherwise a part whose size is a world
40
+ * length. The ribbon is screen-sized for a `{ px }` width; a head for a
41
+ * `{ px }` size, or for no size on a screen-sized ribbon, since it then takes
42
+ * the stroke width.
43
+ */
44
+ interface StrokeSpaces {
45
+ readonly ribbon?: ScreenSpace;
46
+ readonly start?: ScreenSpace;
47
+ readonly mid?: ScreenSpace;
48
+ readonly end?: ScreenSpace;
49
+ }
50
+ /** A cache-key fragment naming `space`; empty for world. */
51
+ declare function screenSpaceKey(space: ScreenSpace | undefined): string;
52
+ /**
53
+ * Split the linear map `x' = a·x + c·y, y' = b·x + d·y` — a GL `mat3`'s
54
+ * `m[0], m[1], m[3], m[4]` — into its geometric-mean scale and its stroke
55
+ * metric. `metric` is `null` when the map is a similarity, where world space
56
+ * already measures like the screen; `scale` is `0` for a singular map.
57
+ */
58
+ declare function strokeSpaceOf(a: number, b: number, c: number, d: number): {
59
+ scale: number;
60
+ metric: StrokeMetric | null;
61
+ };
62
+ /** A cache-key fragment naming `metric`; empty for none. */
63
+ declare function metricKey(metric: StrokeMetric | undefined): string;
64
+ /** Map `pl`'s points through `P`, in place. */
65
+ declare function polylineIntoMetric(pl: Polyline, m: StrokeMetric): void;
66
+ /** Map `pl`'s points back out through `P⁻¹`, in place. */
67
+ declare function polylineOutOfMetric(pl: Polyline, m: StrokeMetric): void;
68
+ /** Whether two spaces are the same — world being `null`. */
69
+ declare function sameMetric(a: StrokeMetric | null, b: StrokeMetric | null): boolean;
70
+ /**
71
+ * What one world unit of width, across a line running along the unit
72
+ * direction `(tx, ty)` of the metric's own space, measures in that space.
73
+ * `P` keeps areas, so a strip keeps its area while its length stretches by
74
+ * `|P u|` (`u` the line's world direction): its width shrinks by the same,
75
+ * and `1 / |P u|` is `|P⁻¹ t|`.
76
+ */
77
+ declare function worldWidthInMetric(m: StrokeMetric, tx: number, ty: number): number;
78
+ /** The same, for a line running along the unit world direction `(ux, uy)`. */
79
+ declare function worldWidthInMetricAlongWorld(m: StrokeMetric, ux: number, uy: number): number;
80
+ /** Map interleaved x,y pairs back out through `P⁻¹` = [[r, −q], [−q, p]], in
81
+ * place. */
82
+ declare function coordsOutOfMetric(coords: Float32Array, m: StrokeMetric): void;
83
+ /** Whether `P` takes an axis-aligned rect to another one. */
84
+ declare function metricKeepsRects(m: StrokeMetric): boolean;
85
+ /** `rect` in the metric's space, while `P` keeps it axis-aligned; otherwise
86
+ * `null`, and the rect has to be carried as a polygon. */
87
+ declare function rectIntoMetric(rect: {
88
+ x: number;
89
+ y: number;
90
+ width: number;
91
+ height: number;
92
+ }, m: StrokeMetric): {
93
+ kind: 'rect';
94
+ x: number;
95
+ y: number;
96
+ width: number;
97
+ height: number;
98
+ } | null;
99
+
100
+ /**
101
+ * Trimming a stroke's ends where each inset is measured.
102
+ *
103
+ * A head stops the line by a length in the head's own space, which need not be
104
+ * the ribbon's: a world-sized head on a `{ px }` stroke stops it a world
105
+ * distance short, a `{ px }` head on a world-width stroke a screen distance.
106
+ * Under a stretching view one length means different distances in the two
107
+ * spaces depending on which way the line runs, so each end is trimmed in its
108
+ * own space rather than converted.
109
+ */
110
+
111
+ /** A length along the line measured in `metric`'s space, or in world where
112
+ * `metric` is `null`. */
113
+ interface MeasuredLength {
114
+ readonly length: number;
115
+ readonly metric: StrokeMetric | null;
116
+ }
117
+ /** A plain number is a length in the ribbon's own space. */
118
+ type InsetLength = number | MeasuredLength;
119
+ /**
120
+ * `pl`, whose points are in `from`'s space, trimmed by `start` and `end` and
121
+ * carried into `ribbon`'s — `null` for world in both. `pl` may be mapped in
122
+ * place. Returns `null` when the insets consume the whole run.
123
+ */
124
+ declare function trimInSpace(pl: Polyline, from: StrokeMetric | null, ribbon: StrokeMetric | null, start: InsetLength | undefined, end: InsetLength | undefined): Polyline | null;
125
+
126
+ /** Options for stroke tessellation. */
127
+ interface StrokeOptions {
128
+ flattenTolerance?: number;
129
+ /**
130
+ * Shorten each open subpath by this much at its start / end: a length in
131
+ * the ribbon's space, or one measured in a space of its own. Stroke markers
132
+ * use this so a filled head is not speared by its own line; the caller
133
+ * resolves the distance, because this layer knows nothing about the marker
134
+ * registry.
135
+ */
136
+ startInset?: InsetLength;
137
+ endInset?: InsetLength;
138
+ /**
139
+ * Build the ribbon in this metric's space and map it back out, so the
140
+ * transform it was derived from draws it at an exact screen width — see
141
+ * `./metric`. Width and dash are then lengths in that space; `vertexWidths`
142
+ * stay world widths. The path is flattened in world first, so the ribbon
143
+ * follows the same vertices its fill does.
144
+ */
145
+ metric?: StrokeMetric;
146
+ }
147
+ /** Resolve a stroke width to world units. A number is already world units;
148
+ * `{ px }` is screen pixels divided by the accumulated scale, so it holds its
149
+ * on-screen thickness as the view zooms. */
150
+ declare function resolveStrokeWidth(width: ScreenLength, scale: number): number;
151
+ /**
152
+ * Build a triangle-mesh ribbon from a stroked Path.
153
+ *
154
+ * Supports:
155
+ * - cap: 'butt' | 'round' | 'square'
156
+ * - join: 'miter' | 'round' | 'bevel'
157
+ * - center alignment directly; RectPath inner/outer alignment via a
158
+ * pre-shifted rect; PolygonPath inner/outer alignment via stencil at the
159
+ * renderer level
160
+ * - dash splitting
161
+ */
162
+ declare function tessellateStroke(path: Path, stroke: Stroke, opts?: StrokeOptions): Mesh;
163
+
164
+ /**
165
+ * Split a polyline into open sub-polylines for the "on" portions of a dash
166
+ * pattern. Each output sub-polyline gets caps from the stroke's `cap` setting.
167
+ * The "off" portions become invisible gaps. Anchor params are carried through:
168
+ * sub-polyline endpoints inherit their source polyline-point's anchor params;
169
+ * dash boundaries that land mid-segment (between two polyline points) snap
170
+ * their anchor params to whichever endpoint they're closer to when the
171
+ * segment crosses a path-anchor boundary, otherwise interpolate `t` linearly.
172
+ */
173
+ declare function splitForDash(pl: Polyline, dash: number[]): Polyline[];
174
+
175
+ /** What one unit of a `vertexWidths` entry measures across the line at a
176
+ * point whose unit tangent, in the polyline's own space, is `(tx, ty)`. */
177
+ type WidthScale = (tx: number, ty: number) => number;
178
+ /** Fill `pl.widths` by interpolating `vertexWidths` across each point's
179
+ * (anchorA, anchorB, anchorT). Anchor-aligned points (A === B) read the
180
+ * anchor's value directly. Out-of-range / non-finite anchor entries fall
181
+ * back to `fallbackWidth`. Given `scale`, each entry is scaled at the point
182
+ * that reads it, and the fallback is not. */
183
+ declare function populatePolylineWidths(pl: Polyline, vertexWidths: number[], fallbackWidth: number, scale?: WidthScale): void;
184
+
185
+ /**
186
+ * The kit's built-in stroke-marker vocabulary.
187
+ *
188
+ * Every entry is authored with its anchor at the origin and pointing +X, so
189
+ * the line arrives from -X and no geometry sits at positive X. Coordinates are
190
+ * in units of `MarkerCtx.size`, which defaults to the resolved stroke width —
191
+ * one definition is therefore correct at any line weight.
192
+ *
193
+ * Not to be confused with `features/paths/markers.ts`, which builds
194
+ * decorative chrome shapes and is unrelated.
195
+ */
196
+
197
+ /** The markers the registry is seeded with: `arrow`, `arrow-open`,
198
+ * `arrow-concave`, `diamond`, `diamond-hollow`, `circle`, `square` and `bar`. */
199
+ declare const BUILTIN_MARKERS: readonly MarkerEntry[];
200
+
201
+ /**
202
+ * Resolving a `MarkerRef` to the distance the ribbon must stop short.
203
+ *
204
+ * The tessellation layer takes these as plain numbers and knows nothing about
205
+ * the registry — keeping that layer free of upward dependencies is deliberate.
206
+ * The ribbon cache, the SVG serializer and `inkReach` all resolve through here,
207
+ * so the ribbon, the export and the hit region cannot disagree about where the
208
+ * line ends.
209
+ */
210
+
211
+ /** The size of one marker unit, in the same world units as `strokeWidth`.
212
+ * `scale` is the view scale a `{ px }` size resolves against, as a `{ px }`
213
+ * stroke width does. */
214
+ declare function resolveMarkerSize(ref: MarkerRef, strokeWidth: number, scale?: number): number;
215
+ /** The registry key a `MarkerRef` names, whether given bare or with a size. */
216
+ declare function markerKeyOf(ref: MarkerRef): string;
217
+ /**
218
+ * How far back the ribbon stops for `ref`, in world units. Zero for an absent
219
+ * marker, an unregistered key, or an open head — never throws, because an
220
+ * unknown key is a data problem and dropping the head is the graceful answer.
221
+ */
222
+ declare function markerInset(ref: MarkerRef | undefined, strokeWidth: number, scale?: number): number;
223
+ /** The start and end insets a stroke asks for. `markerMid` never insets —
224
+ * trimming at an interior vertex would cut the line in two. */
225
+ declare function strokeInsets(stroke: Stroke, strokeWidth: number): {
226
+ start: number;
227
+ end: number;
228
+ };
229
+
230
+ /**
231
+ * Where a stroke's markers sit and which way they point.
232
+ *
233
+ * Computed from the *untrimmed* polyline: trimming moves the endpoint, and a
234
+ * marker anchors to where the line was authored to end, not to where the
235
+ * ribbon was cut.
236
+ */
237
+
238
+ /** One marker placement, in the polyline's own coordinates. */
239
+ interface MarkerSite {
240
+ x: number;
241
+ y: number;
242
+ /** Radians. The direction the marker's +X axis should point — outward at a
243
+ * start or end, along the bisector at an interior vertex. */
244
+ angle: number;
245
+ role: 'start' | 'mid' | 'end';
246
+ }
247
+ /** Which marker roles to place. `start` and `end` are ignored on a closed polyline. */
248
+ interface MarkerSiteRequest {
249
+ start: boolean;
250
+ mid: boolean;
251
+ end: boolean;
252
+ }
253
+ /**
254
+ * Where each requested marker sits on `pl` and the angle it points at.
255
+ * Mid sites fall only on authored vertices, never on flattened curve samples,
256
+ * and need the polyline to carry anchor data; without it no mid sites are returned.
257
+ */
258
+ declare function markerSites(pl: Polyline, want: MarkerSiteRequest): MarkerSite[];
259
+
260
+ /**
261
+ * A stroke's marker heads as placed geometry, and how far they reach.
262
+ */
263
+
264
+ /** One head's placed geometry, before any paint is applied. */
265
+ interface MarkerHead {
266
+ readonly entry: MarkerEntry;
267
+ readonly path: PolygonPath;
268
+ readonly size: number;
269
+ /** Where it was built, when not in world — `size` is in this space's units. */
270
+ readonly space?: ScreenSpace;
271
+ }
272
+ /**
273
+ * Every marker head for `path` under `stroke`, placed but unpainted.
274
+ * `strokeWidth` is the already width-resolved stroke width; `flattenTolerance`
275
+ * matches what the ribbon used, so heads land on the same flattened vertices
276
+ * the stroke did. A head `spaces` puts in a screen space is placed and sized
277
+ * there and comes back in world units, so it meets the line it caps.
278
+ */
279
+ declare function markerHeads(path: Path, stroke: Stroke, strokeWidth: number, flattenTolerance: number | undefined, spaces?: StrokeSpaces): MarkerHead[];
280
+ /**
281
+ * A key for the stroke fields `entry`'s geometry reads, or `null` when the
282
+ * entry does not say which it reads and only the stroke's identity can stand
283
+ * in for them.
284
+ */
285
+ declare function markerReadsKey(entry: MarkerEntry, stroke: Stroke): string | null;
286
+ /**
287
+ * How far `stroke`'s markers can paint from the vertex each sits on, split by
288
+ * where each is built: `world` in world units for a head with a world size,
289
+ * `px` in screen pixels for one with a `{ px }` size. A head with no size
290
+ * takes the stroke width, and with it the width's units. Culling and a path
291
+ * node's grab reach both measure through here.
292
+ */
293
+ declare function markerReach(stroke: Stroke): {
294
+ world: number;
295
+ px: number;
296
+ };
297
+
298
+ /**
299
+ * A stroke as geometry: the region its ink covers, as a fillable path.
300
+ *
301
+ * The ribbon the renderer draws already has the joins, caps, dashes, and
302
+ * marker trims; unioning its triangles gives the silhouette with every
303
+ * self-overlap resolved, so the outline and the drawn stroke cannot disagree.
304
+ */
305
+
306
+ /** Options for {@link strokeToPath}. */
307
+ interface StrokeToPathOptions {
308
+ /** Screen pixels per world unit, for a `{ px }` width or marker size.
309
+ * Default 1. */
310
+ scale?: number;
311
+ /** Flatness tolerance for curves, in the path's units. */
312
+ flattenTolerance?: number;
313
+ /**
314
+ * The frame the stroke is drawn in, as the matrix taking that frame to the
315
+ * path's coordinates: a node's `poseFrameOf`. A frame that skews or
316
+ * stretches does the same to the stroke, as it does on the canvas. For a
317
+ * `{ px }` width under a view that scales each axis apart, the frame is the
318
+ * screen: the inverse of the view's scale, with `scale` left at 1.
319
+ */
320
+ frame?: Mat3;
321
+ }
322
+ /**
323
+ * The region `stroke` covers when drawn along `path`, as a path to fill:
324
+ * width, cap, join, miter limit, dash, alignment, per-vertex widths, and
325
+ * marker heads all included. Curves are flattened, so the result holds only
326
+ * M/L/Z. A stroke with no width answers an empty path, and so does one drawn
327
+ * in a `frame` that flattens it.
328
+ */
329
+ declare function strokeToPath(path: Path, stroke: Stroke, opts?: StrokeToPathOptions): PolygonPath;
330
+
331
+ export { BUILTIN_MARKERS, type InsetLength, MarkerEntry, type MarkerHead, type MarkerSite, type MarkerSiteRequest, type MeasuredLength, type ScreenSpace, type StrokeMetric, type StrokeOptions, type StrokeSpaces, type StrokeToPathOptions, type WidthScale, coordsOutOfMetric, markerHeads, markerInset, markerKeyOf, markerReach, markerReadsKey, markerSites, metricKeepsRects, metricKey, polylineIntoMetric, polylineOutOfMetric, populatePolylineWidths, rectIntoMetric, resolveMarkerSize, resolveStrokeWidth, sameMetric, screenSpaceKey, splitForDash, strokeInsets, strokeSpaceOf, strokeToPath, tessellateStroke, trimInSpace, worldWidthInMetric, worldWidthInMetricAlongWorld };