@weasel-js/stroke 1.15.2 → 1.16.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Path, PolygonPath, Affine } from '@weasel-js/geom';
2
- import { ScreenLength, Stroke, VertexWidth, StrokeNib, MarkerRef } from '@weasel-js/paint';
1
+ import { Path, LengthTable, LengthSpan, CurveSpan, PolygonPath, Affine } from '@weasel-js/geom';
2
+ import { ScreenLength, Stroke, VertexWidth, StrokeNib, StrokeTrim, MarkerRef } from '@weasel-js/paint';
3
3
  import { Polyline, Mesh } from '@weasel-js/geom/tessellate';
4
4
  import { M as MarkerEntry } from './test-seams-D-zMT-bn.js';
5
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';
@@ -173,9 +173,10 @@ declare function dashLength(path: Path): number;
173
173
  * widths and offsets are carried through: a dash boundary landing mid-segment takes
174
174
  * {@link anchorParamsBetween}'s params and the width interpolated there.
175
175
  * A dash {@link dashPattern} reads as solid along `pl` returns `pl` whole,
176
- * and a non-finite one returns nothing.
176
+ * and a non-finite one returns nothing. `phase` is how far into the pattern
177
+ * `pl`'s first point sits, so a run cut from a longer line keeps its pattern.
177
178
  */
178
- declare function splitForDash(pl: Polyline, pattern: readonly number[]): Polyline[];
179
+ declare function splitForDash(pl: Polyline, pattern: readonly number[], phase?: number): Polyline[];
179
180
 
180
181
  /** What one unit of a `vertexWidths` entry measures across the line at a
181
182
  * point whose unit tangent, in the polyline's own space, is `(tx, ty)`. */
@@ -199,6 +200,54 @@ declare function widestStrokeWidth(width: number, vertexWidths: readonly VertexW
199
200
  */
200
201
  declare function nibWidthScale(nib: StrokeNib): WidthScale;
201
202
 
203
+ /**
204
+ * The stretch of `t`'s path a trim draws, as pen-order lengths, or null when
205
+ * it draws nothing: a window starting at or past its end, or a position that
206
+ * is NaN. Positions clamp to the path; no trim is `{}`, the whole of it.
207
+ */
208
+ declare function resolveTrim(t: LengthTable, trim: StrokeTrim | undefined): LengthSpan | null;
209
+
210
+ /**
211
+ * Where `trim` cuts `path`, as curve positions on its corner-resolved
212
+ * outline: resolved once, by exact arc length, so every reader cuts its own
213
+ * flattening at the same points, and a copy carried through a transform
214
+ * command for command takes the positions as they are. Null when it draws
215
+ * nothing; no trim is the whole path.
216
+ */
217
+ declare function trimSpan(path: Path, trim: StrokeTrim | undefined): CurveSpan | null;
218
+
219
+ /** What a trimmed stroke draws, as a path a writer strokes itself. */
220
+ interface TrimmedPath {
221
+ /** One subpath per visible run, in pen order; curves split, not flattened. */
222
+ path: Path;
223
+ /** Per subpath, how far into its pattern a dash starts: the exact arc length from
224
+ * its subpath's start to the cut, the measure a writer's own dashes run in. */
225
+ dashPhases: number[];
226
+ /** Per subpath, whether it came from a closed one, so it takes no start or end marker. */
227
+ fromClosed: boolean[];
228
+ }
229
+ /**
230
+ * The part of `path` that `trim` strokes, cut on the curve points the ribbon
231
+ * cuts at: on the corner-resolved path, at the positions `trimSpan` resolves.
232
+ * A closed subpath the window covers whole stays closed. `null` when the
233
+ * window draws nothing; the input path itself, with no runs listed, when there
234
+ * is no trim.
235
+ */
236
+ declare function trimmedPath(path: Path, trim: StrokeTrim | undefined): TrimmedPath | null;
237
+ /** One stroke a writer states a {@link TrimmedPath} in. */
238
+ interface TrimmedRun {
239
+ path: Path;
240
+ dashPhase: number;
241
+ fromClosed: boolean;
242
+ }
243
+ /**
244
+ * `cut` as the strokes a writer states separately: the whole path as one,
245
+ * unless its subpaths differ in dash phase (when `dashed`) or, with
246
+ * `byClosed`, in whether they came from a closed subpath. A writer that
247
+ * restarts a dash on every subpath, or sets markers per element, needs those apart.
248
+ */
249
+ declare function trimmedRuns(cut: TrimmedPath, dashed: boolean, byClosed: boolean): TrimmedRun[];
250
+
202
251
  /**
203
252
  * The kit's built-in stroke-marker vocabulary.
204
253
  *
@@ -264,7 +313,8 @@ interface MarkerSite {
264
313
  angle: number;
265
314
  role: 'start' | 'mid' | 'end';
266
315
  }
267
- /** Which marker roles to place. `start` and `end` are ignored on a closed polyline. */
316
+ /** Which marker roles to place. `start` and `end` are ignored on a closed polyline,
317
+ * and on a run cut from one. */
268
318
  interface MarkerSiteRequest {
269
319
  start: boolean;
270
320
  mid: boolean;
@@ -274,8 +324,10 @@ interface MarkerSiteRequest {
274
324
  * Where each requested marker sits on `pl` and the angle it points at.
275
325
  * Mid sites fall only on authored vertices, never on flattened curve samples,
276
326
  * and need the polyline to carry anchor data; without it no mid sites are returned.
327
+ * `pl`'s first and last points are never mid sites, so a trimmed run's ends are ends.
328
+ * `fromClosed` marks an open run cut from a closed subpath, which takes no start or end site.
277
329
  */
278
- declare function markerSites(pl: Polyline, want: MarkerSiteRequest): MarkerSite[];
330
+ declare function markerSites(pl: Polyline, want: MarkerSiteRequest, fromClosed?: boolean): MarkerSite[];
279
331
 
280
332
  /**
281
333
  * A stroke's marker heads as placed geometry, and how far they reach.
@@ -294,7 +346,8 @@ interface MarkerHead {
294
346
  * `strokeWidth` is the already width-resolved stroke width; `flattenTolerance`
295
347
  * matches what the ribbon used, so heads land on the same flattened vertices
296
348
  * the stroke did. A head `spaces` puts in a screen space is placed and sized
297
- * there and comes back in world units, so it meets the line it caps.
349
+ * there and comes back in world units, so it meets the line it caps. Under
350
+ * `stroke.trim` heads sit on the visible runs, windowed as the ribbon is.
298
351
  */
299
352
  declare function markerHeads(path: Path, stroke: Stroke, strokeWidth: number, flattenTolerance: number | undefined, spaces?: StrokeSpaces): MarkerHead[];
300
353
  /**
@@ -416,4 +469,4 @@ interface OffsetPathOptions {
416
469
  */
417
470
  declare function offsetPath(path: Path, distance: number, opts?: OffsetPathOptions): PolygonPath;
418
471
 
419
- export { BUILTIN_MARKERS, type InsetLength, MarkerEntry, type MarkerHead, type MarkerSite, type MarkerSiteRequest, type MeasuredLength, type OffsetPathOptions, type ScreenSpace, type StrokeEdgeHit, type StrokeEdgeOptions, type StrokeMetric, type StrokeOptions, type StrokeSide, type StrokeSpaces, type StrokeToPathOptions, type WidthScale, coordsOutOfMetric, dashLength, markerHeads, markerInset, markerKeyOf, markerReach, markerReadsKey, markerSites, metricKeepsRects, metricKey, nibWidthScale, offsetPath, polylineIntoMetric, polylineOutOfMetric, populatePolylineWidths, rectIntoMetric, resolveMarkerSize, resolveStrokeWidth, sameMetric, screenSpaceKey, splitForDash, strokeEdgeNear, strokeInsets, strokeSpaceOf, strokeToPath, tessellateStroke, trimInSpace, widestStrokeWidth, worldWidthInMetric, worldWidthInMetricAlongWorld };
472
+ export { BUILTIN_MARKERS, type InsetLength, MarkerEntry, type MarkerHead, type MarkerSite, type MarkerSiteRequest, type MeasuredLength, type OffsetPathOptions, type ScreenSpace, type StrokeEdgeHit, type StrokeEdgeOptions, type StrokeMetric, type StrokeOptions, type StrokeSide, type StrokeSpaces, type StrokeToPathOptions, type TrimmedPath, type TrimmedRun, type WidthScale, coordsOutOfMetric, dashLength, markerHeads, markerInset, markerKeyOf, markerReach, markerReadsKey, markerSites, metricKeepsRects, metricKey, nibWidthScale, offsetPath, polylineIntoMetric, polylineOutOfMetric, populatePolylineWidths, rectIntoMetric, resolveMarkerSize, resolveStrokeWidth, resolveTrim, sameMetric, screenSpaceKey, splitForDash, strokeEdgeNear, strokeInsets, strokeSpaceOf, strokeToPath, tessellateStroke, trimInSpace, trimSpan, trimmedPath, trimmedRuns, widestStrokeWidth, worldWidthInMetric, worldWidthInMetricAlongWorld };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { getMarker } from './chunk-PNX2RT7L.js';
2
2
  export { BUILTIN_MARKERS, getMarker, listMarkers, markerGeneration, markerRegistry, registerMarker } from './chunk-PNX2RT7L.js';
3
- import { resolveCornersWithSources, remapPerAnchor, segmentT, pointInPath, invert, transformPath, resolveCorners, offsetCornerTraces, restoreCorners, simplifyPath, anchorsToPath, pathToAnchors, DEFAULT_FLATTEN_TOLERANCE, boundsOfPath } from '@weasel-js/geom';
3
+ import { lengthAtSegmentFraction, lengthAtAnchor, lengthAtSubpath, pathLengthTable, curveSpan, resolveCornersWithSources, remapPerAnchor, runSegments, lengthAtCurve, asPolygonPath, splitSubpaths, PATH_L, PATH_Q, PATH_C, PATH_M, PATH_Z, segmentT, pointInPath, invert, transformPath, resolveCorners, offsetCornerTraces, restoreCorners, simplifyPath, anchorsToPath, pathToAnchors, splitQuadraticAt, splitCubicAt, DEFAULT_FLATTEN_TOLERANCE, boundsOfPath, closingAnchor, DEG_TO_RAD } from '@weasel-js/geom';
4
4
  import { dashPattern, widestHalfWidth, resolveScreenLength, alignedStrokeRect, scaleVertexWidth, vertexHalfWidths } from '@weasel-js/paint';
5
- import { extractPolylines, anchorParamsBetween, trimPolyline } from '@weasel-js/geom/tessellate';
5
+ import { extractPolylines, anchorParamsBetween, trimPolyline, polylineAtCurve } from '@weasel-js/geom/tessellate';
6
6
  import { trianglesToPath, pathIntersect, pathSubtract, pathUnion } from '@weasel-js/geom/booleans';
7
7
 
8
8
  function runFrom(p, widths, offsets) {
@@ -28,16 +28,16 @@ function toPolyline(run, closed = false) {
28
28
  if (run.o) pl.offsets = new Float32Array(run.o);
29
29
  return pl;
30
30
  }
31
- function dashPolylines(polylines, pattern) {
32
- const dash = dashPattern(pattern, totalLength(polylines));
31
+ function dashPolylines(polylines, pattern, phases, length = polylinesLength(polylines)) {
32
+ const dash = dashPattern(pattern, length);
33
33
  if (dash === "solid") return [...polylines];
34
34
  if (dash === "non-finite") return [];
35
- return polylines.flatMap((pl) => splitForDash(pl, dash));
35
+ return polylines.flatMap((pl, i) => splitForDash(pl, dash, phases?.[i] ?? 0));
36
36
  }
37
37
  function dashLength(path) {
38
- return totalLength(extractPolylines(path));
38
+ return polylinesLength(extractPolylines(path));
39
39
  }
40
- function totalLength(polylines) {
40
+ function polylinesLength(polylines) {
41
41
  return polylines.reduce((sum, pl) => sum + polylineLength(pl), 0);
42
42
  }
43
43
  function polylineLength(pl) {
@@ -48,7 +48,7 @@ function polylineLength(pl) {
48
48
  if (pl.closed && n > 1) len += Math.hypot(p[0] - p[n * 2 - 2], p[1] - p[n * 2 - 1]);
49
49
  return len;
50
50
  }
51
- function splitForDash(pl, pattern) {
51
+ function splitForDash(pl, pattern, phase = 0) {
52
52
  const dash = dashPattern(pattern, polylineLength(pl));
53
53
  if (dash === "solid") return [pl];
54
54
  if (dash === "non-finite") return [];
@@ -71,7 +71,16 @@ function splitForDash(pl, pattern) {
71
71
  let dashIdx = 0;
72
72
  let dashRemaining = dash[0];
73
73
  let onPhase = true;
74
- let cur = runFrom(pointAt(0), !!W, !!O);
74
+ if (phase > 0) {
75
+ let into = phase % (dash.reduce((a, b) => a + b, 0) * (dash.length % 2 + 1));
76
+ while (into >= dash[dashIdx]) {
77
+ into -= dash[dashIdx];
78
+ dashIdx = (dashIdx + 1) % dash.length;
79
+ onPhase = !onPhase;
80
+ }
81
+ dashRemaining = dash[dashIdx] - into;
82
+ }
83
+ let cur = onPhase ? runFrom(pointAt(0), !!W, !!O) : null;
75
84
  const flushAndAdvance = (at) => {
76
85
  if (cur && cur.pts.length >= 4) out.push(toPolyline(cur));
77
86
  dashIdx = (dashIdx + 1) % dash.length;
@@ -364,9 +373,132 @@ function trimInSpace(pl, from, ribbon, start, end) {
364
373
  moveTo(ribbon);
365
374
  return cur;
366
375
  }
376
+ function resolveTrim(t, trim) {
377
+ const { start = 0, end = 1, pace = "length" } = trim ?? {};
378
+ const from = lengthOf(t, start, pace);
379
+ const to = lengthOf(t, end, pace);
380
+ if (Number.isNaN(from) || Number.isNaN(to) || from >= to) return null;
381
+ return { from, to };
382
+ }
383
+ function lengthOf(t, at, pace) {
384
+ if (typeof at === "number") {
385
+ return pace === "anchor" ? lengthAtSegmentFraction(t, at) : clampLength(t, at * t.total);
386
+ }
387
+ if ("length" in at) return clampLength(t, at.length);
388
+ if ("anchor" in at) return lengthAtAnchor(t, at.anchor);
389
+ return lengthAtSubpath(t, at.subpath);
390
+ }
391
+ var clampLength = (t, s) => Number.isNaN(s) ? NaN : Math.min(Math.max(s, 0), t.total);
392
+
393
+ // src/trim/trimSpan.ts
394
+ function trimSpan(path, trim) {
395
+ const table = pathLengthTable(path);
396
+ const span = resolveTrim(table, trim);
397
+ return span && curveSpan(table, span);
398
+ }
399
+
400
+ // src/trim/windowPolylines.ts
401
+ var EPS = 1e-9;
402
+ function windowPolylines(polylines, span, runs) {
403
+ const out = [];
404
+ for (let r = Math.max(span.from.run, 0); r <= Math.min(span.to.run, polylines.length - 1); r++) {
405
+ const pl = polylines[r];
406
+ const place = (at) => polylineAtCurve(pl, at.segment, at.t, runs?.[r]);
407
+ const head = r === span.from.run ? place(span.from) : null;
408
+ const tail = r === span.to.run ? place(span.to) : null;
409
+ const total = polylinesLength([pl]);
410
+ const lo = head?.length ?? 0;
411
+ const hi = tail?.length ?? total;
412
+ if (hi - lo <= EPS) continue;
413
+ if (pl.closed && lo <= EPS && total - hi <= EPS) {
414
+ out.push({ polyline: pl, fromClosed: true, dashPhase: 0 });
415
+ continue;
416
+ }
417
+ const base = pl.closed ? opened(pl) : pl;
418
+ const cut = trimPolyline(base, lo, total - hi);
419
+ if (!cut) continue;
420
+ let dashPhase = lo;
421
+ if (cut !== base) {
422
+ const q = cut.points;
423
+ const k = q.length - 2;
424
+ if (head && lo > 0) {
425
+ const next = (x, y) => k >= 2 ? Math.hypot(q[2] - x, q[3] - y) : 0;
426
+ dashPhase = Math.max(0, lo + next(q[0], q[1]) - next(head.x, head.y));
427
+ [q[0], q[1]] = [head.x, head.y];
428
+ }
429
+ if (tail && total - hi > 0) [q[k], q[k + 1]] = [tail.x, tail.y];
430
+ }
431
+ out.push({ polyline: cut, fromClosed: pl.closed, dashPhase });
432
+ }
433
+ return out;
434
+ }
435
+ function windowTrim(polylines, path, trim) {
436
+ const span = trimSpan(path, trim);
437
+ if (!span) return null;
438
+ const { runs } = pathLengthTable(path);
439
+ return { span, runs, windowed: windowPolylines(polylines, span, runs) };
440
+ }
441
+ function opened(pl) {
442
+ const n = pl.points.length / 2;
443
+ const { anchorA: A, anchorB: B, anchorT: T, widths: W, offsets: O } = pl;
444
+ const end = closingAnchor(pl) ?? A?.[0] ?? 0;
445
+ const grow = (a, v) => {
446
+ const g = new a.constructor(n + 1);
447
+ g.set(a);
448
+ g[n] = v;
449
+ return g;
450
+ };
451
+ return {
452
+ points: [...pl.points, pl.points[0], pl.points[1]],
453
+ closed: false,
454
+ anchorA: grow(A ?? new Uint32Array(n), end),
455
+ anchorB: grow(B ?? new Uint32Array(n), end),
456
+ anchorT: grow(T ?? new Float32Array(n), 0),
457
+ ...W ? { widths: grow(W, W[0]) } : {},
458
+ ...O ? { offsets: grow(O, O[0]) } : {}
459
+ };
460
+ }
367
461
 
368
- // src/tessellate/ribbon.ts
369
- var ROUND_CAP_STEP_RAD = 10 * Math.PI / 180;
462
+ // src/tessellate/ribbonWindow.ts
463
+ var withOwnPoints = (pl) => ({ ...pl, points: pl.points.slice() });
464
+ var lengthOf2 = (pl) => pl ? polylinesLength([pl]) : 0;
465
+ var worldInset = (inset) => inset === void 0 || typeof inset === "number" || inset.metric === null;
466
+ var insetLength = (inset) => inset === void 0 ? 0 : typeof inset === "number" ? inset : inset.length;
467
+ function windowRibbon(polylines, cut, ribbon, startInset, endInset, dashing) {
468
+ const windowed = cut?.windowed ?? [];
469
+ const inWorld = ribbon === null && worldInset(startInset) && worldInset(endInset);
470
+ const dashLength2 = !dashing ? void 0 : inWorld ? untrimmedWorldLength(polylines, insetLength(startInset), insetLength(endInset)) : polylinesLength(polylines.flatMap((pl) => trimInSpace(withOwnPoints(pl), null, ribbon, startInset, endInset) ?? []));
471
+ const phases = windowed.map((run) => !dashing || run.dashPhase <= 0 || inWorld ? run.dashPhase : measuredPhase(polylines, cut.runs, run, cut.span.from, ribbon, startInset));
472
+ const out = [];
473
+ const kept = [];
474
+ windowed.forEach(({ polyline, fromClosed }, i) => {
475
+ const t = trimInSpace(polyline, null, ribbon, fromClosed ? void 0 : startInset, fromClosed ? void 0 : endInset);
476
+ if (t !== null) {
477
+ out.push(t);
478
+ kept.push(phases[i]);
479
+ }
480
+ });
481
+ return { runs: out, phases: kept, dashLength: dashLength2 };
482
+ }
483
+ function untrimmedWorldLength(polylines, start, end) {
484
+ const cut = Math.max(0, start) + Math.max(0, end);
485
+ let sum = 0;
486
+ for (const pl of polylines) {
487
+ const len = polylinesLength([pl]);
488
+ sum += pl.closed || cut <= 0 ? len : Math.max(0, len - cut);
489
+ }
490
+ return sum;
491
+ }
492
+ function measuredPhase(polylines, runs, run, at, ribbon, startInset) {
493
+ const pl = polylines[at.run];
494
+ const inset = run.fromClosed ? void 0 : startInset;
495
+ const whole = trimInSpace(withOwnPoints(pl), null, ribbon, inset, void 0);
496
+ const toEnd = { from: { ...at, run: 0 }, to: { run: 0, segment: Infinity, t: 1 } };
497
+ const [rest] = windowPolylines([pl], toEnd, runs && [runs[at.run]]);
498
+ const tail = rest ? trimInSpace(withOwnPoints(rest.polyline), null, ribbon, inset, void 0) : null;
499
+ return Math.max(0, lengthOf2(whole) - lengthOf2(tail));
500
+ }
501
+ var ROUND_CAP_STEP_RAD = 10 * DEG_TO_RAD;
370
502
  function emitCap(seg, leftIdx, rightIdx, atEnd, pos, neg, cap, verts, idx, anchorA, anchorB, anchorT, endA, endB, endT) {
371
503
  const sign = atEnd ? 1 : -1;
372
504
  const dx = (seg.bx - seg.ax) / seg.len * sign;
@@ -503,7 +635,7 @@ function emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts
503
635
  return;
504
636
  }
505
637
  }
506
- var ROUND_STEP_RAD = 10 * Math.PI / 180;
638
+ var ROUND_STEP_RAD = 10 * DEG_TO_RAD;
507
639
  function emitRoundJoin(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT, inner) {
508
640
  const cx = a.bx, cy = a.by;
509
641
  const pivotIdx = inner >= 0 ? inner : cornerVertex(a, verts, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
@@ -649,13 +781,25 @@ function tessellateStroke(path, stroke, opts = {}) {
649
781
  const anchorB = [];
650
782
  const anchorT = [];
651
783
  const ribbonSpace = metric ?? null;
652
- const trimmed = [];
653
- for (const pl of polylines) {
654
- populateWidths(pl, vertexWidths, stroke.nib, width, scale);
655
- const t = trimInSpace(pl, rectInMetric ? ribbonSpace : null, ribbonSpace, opts.startInset, opts.endInset);
656
- if (t !== null) trimmed.push(t);
784
+ const from = rectInMetric ? ribbonSpace : null;
785
+ for (const pl of polylines) populateWidths(pl, vertexWidths, stroke.nib, width, scale);
786
+ let runs;
787
+ let phases;
788
+ let dashLength2;
789
+ if (stroke.trim !== void 0) {
790
+ if (from) for (const pl of polylines) polylineOutOfMetric(pl, from);
791
+ ({ runs, phases, dashLength: dashLength2 } = windowRibbon(
792
+ polylines,
793
+ windowTrim(polylines, path, stroke.trim),
794
+ ribbonSpace,
795
+ opts.startInset,
796
+ opts.endInset,
797
+ stroke.dash !== void 0
798
+ ));
799
+ } else {
800
+ runs = polylines.flatMap((pl) => trimInSpace(pl, from, ribbonSpace, opts.startInset, opts.endInset) ?? []);
657
801
  }
658
- for (const sub of dashPolylines(trimmed, stroke.dash)) {
802
+ for (const sub of dashPolylines(runs, stroke.dash, phases, dashLength2)) {
659
803
  expandPolyline(sub, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT);
660
804
  }
661
805
  const vertices = new Float32Array(verts);
@@ -745,6 +889,75 @@ function expandPolyline(pl, width, join, cap, miterLimit, varyingThreshold, vert
745
889
  }
746
890
  var DEFAULT_MITER_LIMIT = 4;
747
891
  var DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD = 1.5;
892
+ var EPS2 = 1e-9;
893
+ function trimmedPath(path, trim) {
894
+ if (trim === void 0) return { path, dashPhases: [], fromClosed: [] };
895
+ const span = trimSpan(path, trim);
896
+ if (!span) return null;
897
+ const table = pathLengthTable(path);
898
+ const out = { path, dashPhases: [], fromClosed: [] };
899
+ const commands = [];
900
+ const coords = [];
901
+ for (let r = span.from.run; r <= span.to.run; r++) {
902
+ const segs = runSegments(table, r);
903
+ const lo = r === span.from.run ? span.from : { run: r, segment: 0, t: 0 };
904
+ const hi = r === span.to.run ? span.to : { run: r, segment: segs - 1, t: 1 };
905
+ const at = table.runStart[r];
906
+ const head = lengthAtCurve(table, lo) - at;
907
+ if (segs <= 0 || lengthAtCurve(table, hi) - at - head <= EPS2) continue;
908
+ const { closed } = table.runs[r];
909
+ const whole = closed && head <= EPS2 && table.runStart[r + 1] - lengthAtCurve(table, hi) <= EPS2;
910
+ writeRun(table, r, lo, hi, whole, commands, coords);
911
+ out.dashPhases.push(whole ? 0 : head);
912
+ out.fromClosed.push(closed);
913
+ }
914
+ if (out.dashPhases.length === 0) return null;
915
+ const shape = path.kind === "rect" ? asPolygonPath(path) : path;
916
+ out.path = {
917
+ kind: "polygon",
918
+ commands: Uint8Array.from(commands),
919
+ coords: Float32Array.from(coords),
920
+ fillRule: shape.fillRule,
921
+ ...shape.origin ? { origin: shape.origin } : {}
922
+ };
923
+ return out;
924
+ }
925
+ function trimmedRuns(cut, dashed, byClosed) {
926
+ const { dashPhases: phases, fromClosed } = cut;
927
+ const alike = (i) => (!byClosed || fromClosed[i] === fromClosed[0]) && (!dashed || phases[i] === phases[0]);
928
+ if (phases.every((_p, i) => alike(i))) {
929
+ return [{ path: cut.path, dashPhase: phases[0] ?? 0, fromClosed: fromClosed[0] ?? false }];
930
+ }
931
+ return splitSubpaths(cut.path).map((path, i) => ({ path, dashPhase: phases[i], fromClosed: fromClosed[i] }));
932
+ }
933
+ function writeRun(table, r, lo, hi, whole, commands, coords) {
934
+ const { points, controls } = table.runs[r];
935
+ const n = points.length / 2;
936
+ for (let k = lo.segment; k <= hi.segment; k++) {
937
+ const ctl = controls?.[k];
938
+ const cmd = !ctl || ctl.length === 0 ? PATH_L : ctl.length === 2 ? PATH_Q : PATH_C;
939
+ if (whole && cmd === PATH_L && k === n - 1) break;
940
+ const j = (k + 1) % n;
941
+ const seg = [points[k * 2], points[k * 2 + 1], ...ctl ? Array.from(ctl) : [], points[j * 2], points[j * 2 + 1]];
942
+ const piece = subSegment(cmd, seg, k === lo.segment ? lo.t : 0, k === hi.segment ? hi.t : 1);
943
+ if (k === lo.segment) {
944
+ commands.push(PATH_M);
945
+ coords.push(piece[0], piece[1]);
946
+ }
947
+ commands.push(cmd);
948
+ for (let i = 2; i < piece.length; i++) coords.push(piece[i]);
949
+ }
950
+ if (whole) commands.push(PATH_Z);
951
+ }
952
+ function subSegment(cmd, c, t0, t1) {
953
+ if (cmd === PATH_L) {
954
+ const at = (t) => [c[0] + (c[2] - c[0]) * t, c[1] + (c[3] - c[1]) * t];
955
+ return [...at(t0), ...at(t1)];
956
+ }
957
+ const split = (p, t) => cmd === PATH_Q ? splitQuadraticAt(p[0], p[1], p[2], p[3], p[4], p[5], t) : splitCubicAt(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], t);
958
+ const head = t1 >= 1 ? c : split(c, t1)[0];
959
+ return t0 <= 0 ? head : split(head, t1 > 0 ? t0 / t1 : 0)[1];
960
+ }
748
961
  function resolveMarkerSize(ref, strokeWidth, scale = 1) {
749
962
  if (typeof ref === "string" || ref.size === void 0) return strokeWidth;
750
963
  return resolveScreenLength(ref.size, scale);
@@ -766,12 +979,12 @@ function strokeInsets(stroke, strokeWidth) {
766
979
  }
767
980
 
768
981
  // src/markerSites.ts
769
- var EPS = 1e-9;
982
+ var EPS3 = 1e-9;
770
983
  function dir(pl, i, j) {
771
984
  const dx = pl.points[j * 2] - pl.points[i * 2];
772
985
  const dy = pl.points[j * 2 + 1] - pl.points[i * 2 + 1];
773
986
  const len = Math.hypot(dx, dy);
774
- if (len < EPS) return null;
987
+ if (len < EPS3) return null;
775
988
  return { x: dx / len, y: dy / len };
776
989
  }
777
990
  function nextDistinct(pl, i, n) {
@@ -782,11 +995,11 @@ function prevDistinct(pl, i) {
782
995
  for (let k = i - 1; k >= 0; k--) if (dir(pl, k, i)) return k;
783
996
  return -1;
784
997
  }
785
- function markerSites(pl, want) {
998
+ function markerSites(pl, want, fromClosed = pl.closed) {
786
999
  const out = [];
787
1000
  const n = pl.points.length / 2;
788
1001
  if (n < 2) return out;
789
- if (want.start && !pl.closed) {
1002
+ if (want.start && !fromClosed) {
790
1003
  const j = nextDistinct(pl, 0, n);
791
1004
  const d = j >= 0 ? dir(pl, 0, j) : null;
792
1005
  if (d) {
@@ -805,7 +1018,7 @@ function markerSites(pl, want) {
805
1018
  const outDir = dir(pl, i, q);
806
1019
  let bx = inDir.x + outDir.x;
807
1020
  let by = inDir.y + outDir.y;
808
- if (Math.hypot(bx, by) < EPS) {
1021
+ if (Math.hypot(bx, by) < EPS3) {
809
1022
  bx = inDir.x;
810
1023
  by = inDir.y;
811
1024
  }
@@ -813,7 +1026,7 @@ function markerSites(pl, want) {
813
1026
  }
814
1027
  }
815
1028
  }
816
- if (want.end && !pl.closed) {
1029
+ if (want.end && !fromClosed) {
817
1030
  const last = n - 1;
818
1031
  const p = prevDistinct(pl, last);
819
1032
  const d = p >= 0 ? dir(pl, p, last) : null;
@@ -848,11 +1061,11 @@ function markerHeads(path, stroke, strokeWidth, flattenTolerance, spaces = {}) {
848
1061
  }
849
1062
  const out = [];
850
1063
  const origin = path.kind === "polygon" ? path.origin : void 0;
851
- for (const pl of extractPolylines(path, strokeFlattening(flattenTolerance, strokeWidth, stroke.vertexWidths))) {
1064
+ for (const { polyline: pl, fromClosed } of headRuns(path, stroke, strokeWidth, flattenTolerance)) {
852
1065
  for (const [space, want] of groups) {
853
1066
  const inSpace = space ? { ...pl, points: pl.points.slice() } : pl;
854
1067
  if (space) polylineIntoMetric(inSpace, space.metric);
855
- for (const site of markerSites(inSpace, want)) {
1068
+ for (const site of markerSites(inSpace, want, fromClosed)) {
856
1069
  const ref = refs[site.role];
857
1070
  if (ref === void 0) continue;
858
1071
  const entry = getMarker(markerKeyOf(ref));
@@ -867,6 +1080,11 @@ function markerHeads(path, stroke, strokeWidth, flattenTolerance, spaces = {}) {
867
1080
  }
868
1081
  return out;
869
1082
  }
1083
+ function headRuns(path, stroke, strokeWidth, flattenTolerance) {
1084
+ const polylines = extractPolylines(path, strokeFlattening(flattenTolerance, strokeWidth, stroke.vertexWidths));
1085
+ if (stroke.trim === void 0) return polylines.map((polyline) => ({ polyline, fromClosed: polyline.closed }));
1086
+ return windowTrim(polylines, path, stroke.trim)?.windowed ?? [];
1087
+ }
870
1088
  function markerReadsKey(entry, stroke) {
871
1089
  if (entry.reads === void 0) return null;
872
1090
  let key = "";
@@ -1048,6 +1266,6 @@ function closedRegion(path) {
1048
1266
  return anchorsToPath(anchors, closed.map(() => true), path);
1049
1267
  }
1050
1268
 
1051
- export { coordsOutOfMetric, dashLength, markerHeads, markerInset, markerKeyOf, markerReach, markerReadsKey, markerSites, metricKeepsRects, metricKey, nibWidthScale, offsetPath, polylineIntoMetric, polylineOutOfMetric, populatePolylineWidths, rectIntoMetric, resolveMarkerSize, resolveStrokeWidth, sameMetric, screenSpaceKey, splitForDash, strokeEdgeNear, strokeInsets, strokeSpaceOf, strokeToPath, tessellateStroke, trimInSpace, widestStrokeWidth, worldWidthInMetric, worldWidthInMetricAlongWorld };
1269
+ export { coordsOutOfMetric, dashLength, markerHeads, markerInset, markerKeyOf, markerReach, markerReadsKey, markerSites, metricKeepsRects, metricKey, nibWidthScale, offsetPath, polylineIntoMetric, polylineOutOfMetric, populatePolylineWidths, rectIntoMetric, resolveMarkerSize, resolveStrokeWidth, resolveTrim, sameMetric, screenSpaceKey, splitForDash, strokeEdgeNear, strokeInsets, strokeSpaceOf, strokeToPath, tessellateStroke, trimInSpace, trimSpan, trimmedPath, trimmedRuns, widestStrokeWidth, worldWidthInMetric, worldWidthInMetricAlongWorld };
1052
1270
  //# sourceMappingURL=index.js.map
1053
1271
  //# sourceMappingURL=index.js.map