@weasel-js/stroke 1.10.0 → 1.11.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.js CHANGED
@@ -1,153 +1,185 @@
1
1
  import { getMarker } from './chunk-FQB7N6YD.js';
2
2
  export { BUILTIN_MARKERS, getMarker, listMarkers, markerGeneration, markerRegistry, registerMarker } from './chunk-FQB7N6YD.js';
3
- import { resolveScreenLength, alignedStrokeRect } from '@weasel-js/paint';
4
- import { trimPolyline, extractPolylines } from '@weasel-js/geom/tessellate';
5
- import { invert, transformPath, boundsOfPath } from '@weasel-js/geom';
3
+ import { resolveCornersWithSources, remapPerAnchor, pointInPath, invert, transformPath, resolveCorners, offsetCornerTraces, restoreCorners, simplifyPath, anchorsToPath, pathToAnchors, DEFAULT_FLATTEN_TOLERANCE, boundsOfPath } from '@weasel-js/geom';
4
+ import { widestHalfWidth, resolveScreenLength, alignedStrokeRect, scaleVertexWidth, vertexHalfWidths } from '@weasel-js/paint';
5
+ import { anchorParamsBetween, trimPolyline, extractPolylines } from '@weasel-js/geom/tessellate';
6
6
  import { trianglesToPath, pathIntersect, pathSubtract, pathUnion } from '@weasel-js/geom/booleans';
7
7
 
8
- // src/tessellate/dash.ts
8
+ function runFrom(p, widths, offsets) {
9
+ return { pts: [p.x, p.y], a: [p.a], b: [p.b], t: [p.t], w: widths ? [p.w] : null, o: offsets ? [p.o] : null };
10
+ }
11
+ function extend(run, p) {
12
+ run.pts.push(p.x, p.y);
13
+ run.a.push(p.a);
14
+ run.b.push(p.b);
15
+ run.t.push(p.t);
16
+ run.w?.push(p.w);
17
+ run.o?.push(p.o);
18
+ }
19
+ function toPolyline(run, closed = false) {
20
+ const pl = {
21
+ points: run.pts,
22
+ closed,
23
+ anchorA: new Uint32Array(run.a),
24
+ anchorB: new Uint32Array(run.b),
25
+ anchorT: new Float32Array(run.t)
26
+ };
27
+ if (run.w) pl.widths = new Float32Array(run.w);
28
+ if (run.o) pl.offsets = new Float32Array(run.o);
29
+ return pl;
30
+ }
9
31
  function splitForDash(pl, dash) {
10
32
  const out = [];
11
- const plA = pl.anchorA ?? new Uint32Array(pl.points.length / 2);
12
- const plB = pl.anchorB ?? new Uint32Array(pl.points.length / 2);
13
- const plT = pl.anchorT ?? new Float32Array(pl.points.length / 2);
33
+ const ptCount = pl.points.length / 2;
34
+ const plA = pl.anchorA ?? new Uint32Array(ptCount);
35
+ const plB = pl.anchorB ?? new Uint32Array(ptCount);
36
+ const plT = pl.anchorT ?? new Float32Array(ptCount);
37
+ const W = pl.widths;
38
+ const O = pl.offsets;
39
+ const pointAt = (i) => ({
40
+ x: pl.points[i * 2],
41
+ y: pl.points[i * 2 + 1],
42
+ a: plA[i],
43
+ b: plB[i],
44
+ t: plT[i],
45
+ w: W ? W[i] : 0,
46
+ o: O ? O[i] : 0
47
+ });
14
48
  let dashIdx = 0;
15
49
  let dashRemaining = dash[0];
16
50
  let onPhase = true;
17
- let curPts = onPhase ? [pl.points[0], pl.points[1]] : null;
18
- let curA = onPhase ? [plA[0]] : null;
19
- let curB = onPhase ? [plB[0]] : null;
20
- let curT = onPhase ? [plT[0]] : null;
21
- const flushAndAdvance = () => {
22
- if (curPts && curPts.length >= 4) {
23
- out.push({
24
- points: curPts,
25
- closed: false,
26
- anchorA: new Uint32Array(curA),
27
- anchorB: new Uint32Array(curB),
28
- anchorT: new Float32Array(curT)
29
- });
30
- }
31
- curPts = null;
32
- curA = null;
33
- curB = null;
34
- curT = null;
51
+ let cur = runFrom(pointAt(0), !!W, !!O);
52
+ const flushAndAdvance = (at) => {
53
+ if (cur && cur.pts.length >= 4) out.push(toPolyline(cur));
35
54
  dashIdx = (dashIdx + 1) % dash.length;
36
55
  dashRemaining = dash[dashIdx];
37
56
  onPhase = !onPhase;
57
+ cur = onPhase ? runFrom(at, !!W, !!O) : null;
38
58
  };
39
59
  let prevX = pl.points[0], prevY = pl.points[1];
40
- const ptCount = pl.points.length / 2;
41
60
  const segCount = pl.closed ? ptCount : ptCount - 1;
42
61
  for (let i = 0; i < segCount; i++) {
43
- const nextIdx = (i + 1) % ptCount;
44
- const cx = pl.points[nextIdx * 2], cy = pl.points[nextIdx * 2 + 1];
45
- const startA = plA[i], startB = plB[i], startT = plT[i];
46
- const endA = plA[nextIdx], endB = plB[nextIdx], endT = plT[nextIdx];
47
- const segFullDx = cx - prevX, segFullDy = cy - prevY;
48
- const segFullLen = Math.hypot(segFullDx, segFullDy);
49
- let segDx = segFullDx, segDy = segFullDy;
62
+ const start = pointAt(i);
63
+ const end = pointAt((i + 1) % ptCount);
64
+ const segFullLen = Math.hypot(end.x - prevX, end.y - prevY);
65
+ if (segFullLen <= 1e-9) {
66
+ if (onPhase && cur) extend(cur, end);
67
+ continue;
68
+ }
50
69
  let segLen = segFullLen;
51
70
  let traveled = 0;
52
71
  while (segLen > 1e-9) {
53
72
  if (segLen <= dashRemaining) {
54
- if (onPhase && curPts) {
55
- curPts.push(cx, cy);
56
- curA.push(endA);
57
- curB.push(endB);
58
- curT.push(endT);
59
- }
73
+ if (onPhase && cur) extend(cur, end);
60
74
  dashRemaining -= segLen;
61
- prevX = cx;
62
- prevY = cy;
63
- traveled = segFullLen;
75
+ prevX = end.x;
76
+ prevY = end.y;
64
77
  segLen = 0;
65
- if (dashRemaining <= 1e-9) {
66
- flushAndAdvance();
67
- if (onPhase) {
68
- curPts = [prevX, prevY];
69
- curA = [endA];
70
- curB = [endB];
71
- curT = [endT];
72
- }
73
- }
78
+ if (dashRemaining <= 1e-9) flushAndAdvance(end);
74
79
  } else {
75
- const tConsume = dashRemaining / segLen;
76
- const ix = prevX + segDx * tConsume;
77
- const iy = prevY + segDy * tConsume;
78
80
  traveled += dashRemaining;
79
81
  const frac = traveled / segFullLen;
80
- let mA, mB, mT;
81
- if (startA === endA && startB === endB) {
82
- mA = startA;
83
- mB = startB;
84
- mT = startT + (endT - startT) * frac;
85
- } else {
86
- if (frac < 0.5) {
87
- mA = startA;
88
- mB = startB;
89
- mT = startT;
90
- } else {
91
- mA = endA;
92
- mB = endB;
93
- mT = endT;
94
- }
95
- }
96
- if (onPhase && curPts) {
97
- curPts.push(ix, iy);
98
- curA.push(mA);
99
- curB.push(mB);
100
- curT.push(mT);
101
- }
102
- prevX = ix;
103
- prevY = iy;
104
- segDx = cx - prevX;
105
- segDy = cy - prevY;
106
- segLen = Math.hypot(segDx, segDy);
107
- flushAndAdvance();
108
- if (onPhase) {
109
- curPts = [prevX, prevY];
110
- curA = [mA];
111
- curB = [mB];
112
- curT = [mT];
113
- }
82
+ const [a, b, t] = anchorParamsBetween(start.a, start.b, start.t, end.a, end.b, end.t, frac);
83
+ const mid = {
84
+ x: start.x + (end.x - start.x) * frac,
85
+ y: start.y + (end.y - start.y) * frac,
86
+ a,
87
+ b,
88
+ t,
89
+ w: start.w + (end.w - start.w) * frac,
90
+ o: start.o + (end.o - start.o) * frac
91
+ };
92
+ if (onPhase && cur) extend(cur, mid);
93
+ prevX = mid.x;
94
+ prevY = mid.y;
95
+ segLen = Math.hypot(end.x - prevX, end.y - prevY);
96
+ flushAndAdvance(mid);
114
97
  }
115
98
  }
116
99
  }
117
- if (curPts && curPts.length >= 4) {
118
- const tail = {
119
- points: curPts,
120
- closed: false,
121
- anchorA: new Uint32Array(curA),
122
- anchorB: new Uint32Array(curB),
123
- anchorT: new Float32Array(curT)
124
- };
125
- const tx = curPts[curPts.length - 2], ty = curPts[curPts.length - 1];
126
- const meets = (x, y) => Math.hypot(tx - x, ty - y) <= 1e-9;
127
- if (pl.closed && out.length > 0 && meets(out[0].points[0], out[0].points[1])) {
128
- out[0] = joinAtSeam(tail, out[0]);
129
- } else if (pl.closed && out.length === 0 && meets(curPts[0], curPts[1])) {
130
- out.push({
131
- points: curPts.slice(0, -2),
132
- closed: true,
133
- anchorA: tail.anchorA.slice(0, -1),
134
- anchorB: tail.anchorB.slice(0, -1),
135
- anchorT: tail.anchorT.slice(0, -1)
136
- });
137
- } else {
138
- out.push(tail);
139
- }
140
- }
100
+ const tail = cur;
101
+ if (tail && tail.pts.length >= 4) closeOut(pl, tail, out);
141
102
  return out;
142
103
  }
104
+ function closeOut(pl, tail, out) {
105
+ const tx = tail.pts[tail.pts.length - 2], ty = tail.pts[tail.pts.length - 1];
106
+ const meets = (x, y) => Math.hypot(tx - x, ty - y) <= 1e-9;
107
+ if (pl.closed && out.length > 0 && meets(out[0].points[0], out[0].points[1])) {
108
+ out[0] = joinAtSeam(toPolyline(tail), out[0]);
109
+ } else if (pl.closed && out.length === 0 && meets(tail.pts[0], tail.pts[1])) {
110
+ out.push(toPolyline({
111
+ pts: tail.pts.slice(0, -2),
112
+ a: tail.a.slice(0, -1),
113
+ b: tail.b.slice(0, -1),
114
+ t: tail.t.slice(0, -1),
115
+ w: tail.w ? tail.w.slice(0, -1) : null,
116
+ o: tail.o ? tail.o.slice(0, -1) : null
117
+ }, true));
118
+ } else {
119
+ out.push(toPolyline(tail));
120
+ }
121
+ }
143
122
  function joinAtSeam(tail, head) {
144
- return {
123
+ const pl = {
145
124
  points: [...tail.points, ...head.points.slice(2)],
146
125
  closed: false,
147
126
  anchorA: Uint32Array.from([...tail.anchorA, ...head.anchorA.subarray(1)]),
148
127
  anchorB: Uint32Array.from([...tail.anchorB, ...head.anchorB.subarray(1)]),
149
128
  anchorT: Float32Array.from([...tail.anchorT, ...head.anchorT.subarray(1)])
150
129
  };
130
+ if (tail.widths && head.widths) pl.widths = Float32Array.from([...tail.widths, ...head.widths.subarray(1)]);
131
+ if (tail.offsets && head.offsets) pl.offsets = Float32Array.from([...tail.offsets, ...head.offsets.subarray(1)]);
132
+ return pl;
133
+ }
134
+
135
+ // src/tessellate/nib.ts
136
+ function nibWidthScale(nib) {
137
+ const ex = Math.cos(nib.angle), ey = -Math.sin(nib.angle);
138
+ const r = Math.min(1, Math.max(0, nib.ratio));
139
+ return (tx, ty) => {
140
+ const across = ex * ty - ey * tx;
141
+ const along = ex * tx + ey * ty;
142
+ return Math.sqrt(across * across + r * r * along * along);
143
+ };
144
+ }
145
+ function applyNib(pl, nib, base) {
146
+ const n = pl.points.length / 2;
147
+ if (n < 2) return;
148
+ const P = pl.points;
149
+ const scale = nibWidthScale(nib);
150
+ const segCount = pl.closed ? n : n - 1;
151
+ const factors = [];
152
+ for (let i = 0; i < segCount; i++) {
153
+ const j = (i + 1) % n;
154
+ const dx = P[j * 2] - P[i * 2], dy = P[j * 2 + 1] - P[i * 2 + 1];
155
+ const len = Math.hypot(dx, dy);
156
+ factors.push(len > 0 ? scale(dx / len, dy / len) : NaN);
157
+ }
158
+ for (let i = 0; i < segCount; i++) {
159
+ if (Number.isNaN(factors[i])) factors[i] = i > 0 ? factors[i - 1] : factors.find((f) => !Number.isNaN(f)) ?? 1;
160
+ }
161
+ const A = pl.anchorA, B = pl.anchorB, T = pl.anchorT, W = pl.widths, O = pl.offsets;
162
+ const pts = [], aA = [], aB = [], aT = [], ws = [], os = [];
163
+ const push = (i, factor) => {
164
+ pts.push(P[i * 2], P[i * 2 + 1]);
165
+ aA.push(A ? A[i] : 0);
166
+ aB.push(B ? B[i] : 0);
167
+ aT.push(T ? T[i] : 0);
168
+ ws.push((W ? W[i] : base) * factor);
169
+ if (O) os.push(O[i] * factor);
170
+ };
171
+ push(0, factors[0]);
172
+ for (let i = 1; i < n; i++) {
173
+ push(i, factors[i - 1]);
174
+ if (i < segCount) push(i, factors[i]);
175
+ }
176
+ if (pl.closed) push(0, factors[segCount - 1]);
177
+ pl.points = pts;
178
+ pl.anchorA = Uint32Array.from(aA);
179
+ pl.anchorB = Uint32Array.from(aB);
180
+ pl.anchorT = Float32Array.from(aT);
181
+ pl.widths = Float32Array.from(ws);
182
+ if (O) pl.offsets = Float32Array.from(os);
151
183
  }
152
184
 
153
185
  // src/tessellate/vertexWidths.ts
@@ -158,23 +190,36 @@ function populatePolylineWidths(pl, vertexWidths, fallbackWidth, scale) {
158
190
  const aB = pl.anchorB ?? new Uint32Array(ptCount);
159
191
  const aT = pl.anchorT ?? new Float32Array(ptCount);
160
192
  const widths = new Float32Array(ptCount);
193
+ const offsets = new Float32Array(ptCount);
194
+ let sided = false;
195
+ const fallback = fallbackWidth / 2;
161
196
  let k = 1;
162
- const read = (i) => {
163
- const v = vertexWidths[i];
164
- return typeof v === "number" && isFinite(v) && v > 0 ? v * k : fallbackWidth;
197
+ const sides = (i) => {
198
+ const h = vertexHalfWidths(vertexWidths[i], NaN);
199
+ return [Number.isNaN(h.left) ? fallback : h.left * k, Number.isNaN(h.right) ? fallback : h.right * k];
165
200
  };
166
201
  for (let i = 0; i < ptCount; i++) {
167
202
  if (scale) k = scaleAt(pl, i, scale);
168
203
  const a = aA[i], b = aB[i], t = aT[i];
169
- if (a === b) {
170
- widths[i] = read(a);
171
- } else {
172
- const wa = read(a);
173
- const wb = read(b);
174
- widths[i] = wa + (wb - wa) * t;
204
+ let [left, right] = sides(a);
205
+ if (a !== b) {
206
+ const [lb, rb] = sides(b);
207
+ left += (lb - left) * t;
208
+ right += (rb - right) * t;
175
209
  }
210
+ widths[i] = left + right;
211
+ offsets[i] = (right - left) / 2;
212
+ if (offsets[i] !== 0) sided = true;
176
213
  }
177
214
  pl.widths = widths;
215
+ if (sided) pl.offsets = offsets;
216
+ }
217
+ function widestStrokeWidth(width, vertexWidths) {
218
+ return 2 * widestHalfWidth(width, vertexWidths);
219
+ }
220
+ function populateWidths(pl, vertexWidths, nib, width, scale) {
221
+ if (vertexWidths) populatePolylineWidths(pl, vertexWidths, width, scale);
222
+ if (nib) applyNib(pl, nib, width);
178
223
  }
179
224
  function scaleAt(pl, i, scale) {
180
225
  const pts = pl.points;
@@ -196,6 +241,14 @@ function scaleAt(pl, i, scale) {
196
241
  return len > 0 ? scale(tx / len, ty / len) : 1;
197
242
  }
198
243
 
244
+ // src/tessellate/flattening.ts
245
+ var STROKE_MAX_TURN = Math.PI / 18;
246
+ function strokeFlattening(flattenTolerance, width, vertexWidths) {
247
+ const tolerance = flattenTolerance ?? DEFAULT_FLATTEN_TOLERANCE;
248
+ const widest = widestStrokeWidth(width, vertexWidths);
249
+ return widest / 2 > tolerance ? { flattenTolerance, maxTurn: STROKE_MAX_TURN } : { flattenTolerance };
250
+ }
251
+
199
252
  // src/tessellate/metric.ts
200
253
  function screenSpaceKey(space) {
201
254
  return space === void 0 ? "" : `${metricKey(space.metric)}@${space.pxPerUnit}`;
@@ -290,150 +343,16 @@ function trimInSpace(pl, from, ribbon, start, end) {
290
343
  return cur;
291
344
  }
292
345
 
293
- // src/tessellate/stroke.ts
294
- var EMPTY_MESH = {
295
- vertices: new Float32Array(0),
296
- indices: new Uint32Array(0),
297
- anchorA: new Uint32Array(0),
298
- anchorB: new Uint32Array(0),
299
- anchorT: new Float32Array(0)
300
- };
301
- function resolveStrokeWidth(width, scale) {
302
- return resolveScreenLength(width, scale);
303
- }
304
- function tessellateStroke(path, stroke, opts = {}) {
305
- const width = resolveStrokeWidth(stroke.width ?? 1, 1);
306
- if (width <= 0) return EMPTY_MESH;
307
- const join = stroke.join ?? "miter";
308
- const cap = stroke.cap ?? "butt";
309
- const align = stroke.align ?? "center";
310
- const miterLimit = stroke.miterLimit ?? DEFAULT_MITER_LIMIT;
311
- const varyingThreshold = stroke.varyingWidthJoinThreshold ?? DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD;
312
- const metric = opts.metric;
313
- const rectInMetric = metric && path.kind === "rect" ? rectIntoMetric(path, metric) : null;
314
- let workingPath = rectInMetric ?? path;
315
- if (workingPath.kind === "rect" && align !== "center") {
316
- const aligned = alignedStrokeRect(workingPath, align, width);
317
- workingPath = {
318
- kind: "rect",
319
- x: aligned.x,
320
- y: aligned.y,
321
- width: aligned.width,
322
- height: aligned.height
323
- };
324
- }
325
- const polylines = extractPolylines(workingPath, opts);
326
- const vertexWidths = stroke.vertexWidths && stroke.vertexWidths.length > 0 ? stroke.vertexWidths : null;
327
- const scale = !metric ? void 0 : rectInMetric ? (tx, ty) => worldWidthInMetric(metric, tx, ty) : (ux, uy) => worldWidthInMetricAlongWorld(metric, ux, uy);
328
- const dash = stroke.dash ?? [];
329
- const verts = [];
330
- const idx = [];
331
- const anchorA = [];
332
- const anchorB = [];
333
- const anchorT = [];
334
- const ribbonSpace = metric ?? null;
335
- for (const pl of polylines) {
336
- if (vertexWidths) populatePolylineWidths(pl, vertexWidths, width, scale);
337
- const trimmed = trimInSpace(pl, rectInMetric ? ribbonSpace : null, ribbonSpace, opts.startInset, opts.endInset);
338
- if (trimmed === null) continue;
339
- const subs = dash.length > 0 ? splitForDash(trimmed, dash) : [trimmed];
340
- for (const sub of subs) {
341
- expandPolyline(sub, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT);
342
- }
343
- }
344
- const vertices = new Float32Array(verts);
345
- if (metric) coordsOutOfMetric(vertices, metric);
346
- return {
347
- vertices,
348
- indices: new Uint32Array(idx),
349
- anchorA: new Uint32Array(anchorA),
350
- anchorB: new Uint32Array(anchorB),
351
- anchorT: new Float32Array(anchorT)
352
- };
353
- }
354
- function expandPolyline(pl, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT) {
355
- const half = width / 2;
356
- const pts = pl.points;
357
- const ptCount = pts.length / 2;
358
- const segCount = ptCount - 1;
359
- if (segCount < 1) return;
360
- const plA = pl.anchorA ?? new Uint32Array(ptCount);
361
- const plB = pl.anchorB ?? new Uint32Array(ptCount);
362
- const plT = pl.anchorT ?? new Float32Array(ptCount);
363
- const plHalf = pl.widths ? pl.widths.map((w) => w / 2) : null;
364
- const segs = [];
365
- const segSrcIdx = [];
366
- for (let s = 0; s < segCount; s++) {
367
- const ha = plHalf ? plHalf[s] : half;
368
- const hb = plHalf ? plHalf[s + 1] : half;
369
- const seg = makeSeg(pts[s * 2], pts[s * 2 + 1], pts[(s + 1) * 2], pts[(s + 1) * 2 + 1], ha, hb);
370
- if (seg) {
371
- segs.push(seg);
372
- segSrcIdx.push(s);
373
- }
374
- }
375
- let closerSrcIdx = -1;
376
- if (pl.closed && segs.length >= 1) {
377
- const last = segs[segs.length - 1];
378
- const first = segs[0];
379
- const haCloser = plHalf ? plHalf[ptCount - 1] : half;
380
- const hbCloser = plHalf ? plHalf[0] : half;
381
- const closer = makeSeg(last.bx, last.by, first.ax, first.ay, haCloser, hbCloser);
382
- if (closer) {
383
- segs.push(closer);
384
- closerSrcIdx = ptCount - 1;
385
- segSrcIdx.push(closerSrcIdx);
386
- }
387
- }
388
- if (segs.length === 0) return;
389
- const segBaseIdx = [];
390
- for (let s = 0; s < segs.length; s++) {
391
- const seg = segs[s];
392
- const startSrc = segSrcIdx[s];
393
- const endSrc = s === segs.length - 1 && pl.closed && closerSrcIdx >= 0 ? 0 : startSrc + 1;
394
- const base = verts.length / 2;
395
- segBaseIdx.push(base);
396
- const nxA = seg.nxUnit * seg.halfA, nyA = seg.nyUnit * seg.halfA;
397
- const nxB = seg.nxUnit * seg.halfB, nyB = seg.nyUnit * seg.halfB;
398
- verts.push(seg.ax + nxA, seg.ay + nyA);
399
- anchorA.push(plA[startSrc]);
400
- anchorB.push(plB[startSrc]);
401
- anchorT.push(plT[startSrc]);
402
- verts.push(seg.ax - nxA, seg.ay - nyA);
403
- anchorA.push(plA[startSrc]);
404
- anchorB.push(plB[startSrc]);
405
- anchorT.push(plT[startSrc]);
406
- verts.push(seg.bx + nxB, seg.by + nyB);
407
- anchorA.push(plA[endSrc]);
408
- anchorB.push(plB[endSrc]);
409
- anchorT.push(plT[endSrc]);
410
- verts.push(seg.bx - nxB, seg.by - nyB);
411
- anchorA.push(plA[endSrc]);
412
- anchorB.push(plB[endSrc]);
413
- anchorT.push(plT[endSrc]);
414
- idx.push(base, base + 1, base + 2, base + 1, base + 3, base + 2);
415
- }
416
- const joinCount = pl.closed ? segs.length : segs.length - 1;
417
- for (let j = 0; j < joinCount; j++) {
418
- emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT, segSrcIdx, plA, plB, plT);
419
- }
420
- if (!pl.closed && cap !== "butt") {
421
- const first = segs[0];
422
- const firstBase = segBaseIdx[0];
423
- emitCap(first, firstBase + 0, firstBase + 1, false, first.halfA, cap, verts, idx, anchorA, anchorB, anchorT, plA[0], plB[0], plT[0]);
424
- const last = segs[segs.length - 1];
425
- const lastBase = segBaseIdx[segs.length - 1];
426
- const lastSrc = segSrcIdx[segs.length - 1] + 1;
427
- emitCap(last, lastBase + 2, lastBase + 3, true, last.halfB, cap, verts, idx, anchorA, anchorB, anchorT, plA[lastSrc], plB[lastSrc], plT[lastSrc]);
428
- }
429
- }
346
+ // src/tessellate/ribbon.ts
430
347
  var ROUND_CAP_STEP_RAD = 10 * Math.PI / 180;
431
- function emitCap(seg, leftIdx, rightIdx, atEnd, half, cap, verts, idx, anchorA, anchorB, anchorT, endA, endB, endT) {
348
+ function emitCap(seg, leftIdx, rightIdx, atEnd, pos, neg, cap, verts, idx, anchorA, anchorB, anchorT, endA, endB, endT) {
432
349
  const sign = atEnd ? 1 : -1;
433
350
  const dx = (seg.bx - seg.ax) / seg.len * sign;
434
351
  const dy = (seg.by - seg.ay) / seg.len * sign;
435
- const cx = atEnd ? seg.bx : seg.ax;
436
- const cy = atEnd ? seg.by : seg.ay;
352
+ const half = (pos + neg) / 2;
353
+ const shift = (pos - neg) / 2;
354
+ const cx = (atEnd ? seg.bx : seg.ax) + seg.nxUnit * shift;
355
+ const cy = (atEnd ? seg.by : seg.ay) + seg.nyUnit * shift;
437
356
  const nx = seg.nxUnit * half;
438
357
  const ny = seg.nyUnit * half;
439
358
  if (cap === "square") {
@@ -484,7 +403,7 @@ function emitCap(seg, leftIdx, rightIdx, atEnd, half, cap, verts, idx, anchorA,
484
403
  else idx.push(prevIdx, pivotIdx, rightIdx);
485
404
  }
486
405
  }
487
- function makeSeg(ax, ay, bx, by, halfA, halfB) {
406
+ function makeSeg(ax, ay, bx, by, posA, negA, posB, negB) {
488
407
  const dx = bx - ax, dy = by - ay;
489
408
  const len = Math.hypot(dx, dy);
490
409
  if (len === 0) return null;
@@ -495,13 +414,13 @@ function makeSeg(ax, ay, bx, by, halfA, halfB) {
495
414
  by,
496
415
  nxUnit: -dy / len,
497
416
  nyUnit: dx / len,
498
- halfA,
499
- halfB,
417
+ posA,
418
+ negA,
419
+ posB,
420
+ negB,
500
421
  len
501
422
  };
502
423
  }
503
- var DEFAULT_MITER_LIMIT = 4;
504
- var DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD = 1.5;
505
424
  function emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT, segSrcIdx, plA, plB, plT) {
506
425
  const a = segs[j];
507
426
  const b = segs[(j + 1) % segs.length];
@@ -519,9 +438,11 @@ function emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts
519
438
  const cornerA = plA[cornerSrc];
520
439
  const cornerB = plB[cornerSrc];
521
440
  const cornerT = plT[cornerSrc];
522
- const halfAtCorner = a.halfB;
523
- const taperA = Math.max(a.halfA, a.halfB) / Math.max(1e-9, Math.min(a.halfA, a.halfB));
524
- const taperB = Math.max(b.halfA, b.halfB) / Math.max(1e-9, Math.min(b.halfA, b.halfB));
441
+ const outerA = onPositive ? [a.negA, a.negB] : [a.posA, a.posB];
442
+ const outerB = onPositive ? [b.negA, b.negB] : [b.posA, b.posB];
443
+ const halfAtCorner = outerA[1];
444
+ const taperA = Math.max(...outerA) / Math.max(1e-9, Math.min(...outerA));
445
+ const taperB = Math.max(...outerB) / Math.max(1e-9, Math.min(...outerB));
525
446
  const forceBevelForVarying = taperA > varyingThreshold || taperB > varyingThreshold;
526
447
  if (join === "bevel" || forceBevelForVarying) {
527
448
  emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
@@ -576,7 +497,7 @@ function emitRoundJoin(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchor
576
497
  const startY = verts[aOuterEnd * 2 + 1] - cy;
577
498
  const endX = verts[bOuterStart * 2] - cx;
578
499
  const endY = verts[bOuterStart * 2 + 1] - cy;
579
- const r = Math.hypot(startX, startY);
500
+ const r0 = Math.hypot(startX, startY), r1 = Math.hypot(endX, endY);
580
501
  const startAngle = Math.atan2(startY, startX);
581
502
  const endAngle = Math.atan2(endY, endX);
582
503
  let sweep = endAngle - startAngle;
@@ -587,10 +508,9 @@ function emitRoundJoin(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchor
587
508
  let prevIdx = aOuterEnd;
588
509
  for (let i = 1; i < steps; i++) {
589
510
  const ang = startAngle + i * stepAngle;
590
- const fx = cx + Math.cos(ang) * r;
591
- const fy = cy + Math.sin(ang) * r;
592
- const newIdx = verts.length / 2;
593
- verts.push(fx, fy);
511
+ const r = r0 + (r1 - r0) * i / steps;
512
+ verts.push(cx + Math.cos(ang) * r, cy + Math.sin(ang) * r);
513
+ const newIdx = verts.length / 2 - 1;
594
514
  anchorA.push(cornerA);
595
515
  anchorB.push(cornerB);
596
516
  anchorT.push(cornerT);
@@ -616,6 +536,139 @@ function lineLineIntersect(apx, apy, adx, ady, bpx, bpy, bdx, bdy) {
616
536
  const t = ((bpx - apx) * bdy - (bpy - apy) * bdx) / denom;
617
537
  return [apx + t * adx, apy + t * ady];
618
538
  }
539
+
540
+ // src/tessellate/stroke.ts
541
+ var EMPTY_MESH = {
542
+ vertices: new Float32Array(0),
543
+ indices: new Uint32Array(0),
544
+ anchorA: new Uint32Array(0),
545
+ anchorB: new Uint32Array(0),
546
+ anchorT: new Float32Array(0)
547
+ };
548
+ function resolveStrokeWidth(width, scale) {
549
+ return resolveScreenLength(width, scale);
550
+ }
551
+ function tessellateStroke(path, stroke, opts = {}) {
552
+ const width = resolveStrokeWidth(stroke.width ?? 1, 1);
553
+ if (width <= 0) return EMPTY_MESH;
554
+ const join = stroke.join ?? "miter";
555
+ const cap = stroke.cap ?? "butt";
556
+ const align = stroke.align ?? "center";
557
+ const miterLimit = stroke.miterLimit ?? DEFAULT_MITER_LIMIT;
558
+ const varyingThreshold = stroke.varyingWidthJoinThreshold ?? DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD;
559
+ const metric = opts.metric;
560
+ const rectInMetric = metric && path.kind === "rect" ? rectIntoMetric(path, metric) : null;
561
+ let workingPath = rectInMetric ?? path;
562
+ if (workingPath.kind === "rect" && align !== "center") {
563
+ workingPath = { kind: "rect", ...alignedStrokeRect(workingPath, align, width) };
564
+ }
565
+ const resolved = workingPath.kind === "polygon" ? resolveCornersWithSources(workingPath) : null;
566
+ if (resolved) workingPath = resolved.path;
567
+ const given = stroke.vertexWidths && stroke.vertexWidths.length > 0 ? stroke.vertexWidths : null;
568
+ const vertexWidths = given && resolved?.anchorSources ? remapPerAnchor(given, resolved.anchorSources, 1) : given;
569
+ const polylines = extractPolylines(workingPath, strokeFlattening(opts.flattenTolerance, width, vertexWidths ?? void 0));
570
+ const scale = !metric ? void 0 : rectInMetric ? (tx, ty) => worldWidthInMetric(metric, tx, ty) : (ux, uy) => worldWidthInMetricAlongWorld(metric, ux, uy);
571
+ const dash = stroke.dash ?? [];
572
+ const verts = [];
573
+ const idx = [];
574
+ const anchorA = [];
575
+ const anchorB = [];
576
+ const anchorT = [];
577
+ const ribbonSpace = metric ?? null;
578
+ for (const pl of polylines) {
579
+ populateWidths(pl, vertexWidths, stroke.nib, width, scale);
580
+ const trimmed = trimInSpace(pl, rectInMetric ? ribbonSpace : null, ribbonSpace, opts.startInset, opts.endInset);
581
+ if (trimmed === null) continue;
582
+ const subs = dash.length > 0 ? splitForDash(trimmed, dash) : [trimmed];
583
+ for (const sub of subs) {
584
+ expandPolyline(sub, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT);
585
+ }
586
+ }
587
+ const vertices = new Float32Array(verts);
588
+ if (metric) coordsOutOfMetric(vertices, metric);
589
+ return {
590
+ vertices,
591
+ indices: new Uint32Array(idx),
592
+ anchorA: new Uint32Array(anchorA),
593
+ anchorB: new Uint32Array(anchorB),
594
+ anchorT: new Float32Array(anchorT)
595
+ };
596
+ }
597
+ function expandPolyline(pl, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT) {
598
+ const half = width / 2;
599
+ const pts = pl.points;
600
+ const ptCount = pts.length / 2;
601
+ const segCount = ptCount - 1;
602
+ if (segCount < 1) return;
603
+ const plA = pl.anchorA ?? new Uint32Array(ptCount);
604
+ const plB = pl.anchorB ?? new Uint32Array(ptCount);
605
+ const plT = pl.anchorT ?? new Float32Array(ptCount);
606
+ const W = pl.widths, O = pl.offsets;
607
+ const pos = (i) => (W ? W[i] / 2 : half) + (O ? O[i] : 0);
608
+ const neg = (i) => (W ? W[i] / 2 : half) - (O ? O[i] : 0);
609
+ const segs = [];
610
+ const segSrcIdx = [];
611
+ for (let s = 0; s < segCount; s++) {
612
+ const seg = makeSeg(pts[s * 2], pts[s * 2 + 1], pts[(s + 1) * 2], pts[(s + 1) * 2 + 1], pos(s), neg(s), pos(s + 1), neg(s + 1));
613
+ if (seg) {
614
+ segs.push(seg);
615
+ segSrcIdx.push(s);
616
+ }
617
+ }
618
+ let closerSrcIdx = -1;
619
+ if (pl.closed && segs.length >= 1) {
620
+ const last = segs[segs.length - 1];
621
+ const first = segs[0];
622
+ const closer = makeSeg(last.bx, last.by, first.ax, first.ay, pos(ptCount - 1), neg(ptCount - 1), pos(0), neg(0));
623
+ if (closer) {
624
+ segs.push(closer);
625
+ closerSrcIdx = ptCount - 1;
626
+ segSrcIdx.push(closerSrcIdx);
627
+ }
628
+ }
629
+ if (segs.length === 0) return;
630
+ const segBaseIdx = [];
631
+ for (let s = 0; s < segs.length; s++) {
632
+ const seg = segs[s];
633
+ const startSrc = segSrcIdx[s];
634
+ const endSrc = s === segs.length - 1 && pl.closed && closerSrcIdx >= 0 ? 0 : startSrc + 1;
635
+ const base = verts.length / 2;
636
+ segBaseIdx.push(base);
637
+ const { nxUnit: nx, nyUnit: ny } = seg;
638
+ verts.push(seg.ax + nx * seg.posA, seg.ay + ny * seg.posA);
639
+ anchorA.push(plA[startSrc]);
640
+ anchorB.push(plB[startSrc]);
641
+ anchorT.push(plT[startSrc]);
642
+ verts.push(seg.ax - nx * seg.negA, seg.ay - ny * seg.negA);
643
+ anchorA.push(plA[startSrc]);
644
+ anchorB.push(plB[startSrc]);
645
+ anchorT.push(plT[startSrc]);
646
+ verts.push(seg.bx + nx * seg.posB, seg.by + ny * seg.posB);
647
+ anchorA.push(plA[endSrc]);
648
+ anchorB.push(plB[endSrc]);
649
+ anchorT.push(plT[endSrc]);
650
+ verts.push(seg.bx - nx * seg.negB, seg.by - ny * seg.negB);
651
+ anchorA.push(plA[endSrc]);
652
+ anchorB.push(plB[endSrc]);
653
+ anchorT.push(plT[endSrc]);
654
+ idx.push(base, base + 1, base + 2, base + 1, base + 3, base + 2);
655
+ }
656
+ const joinCount = pl.closed ? segs.length : segs.length - 1;
657
+ for (let j = 0; j < joinCount; j++) {
658
+ emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT, segSrcIdx, plA, plB, plT);
659
+ }
660
+ if (!pl.closed && cap !== "butt") {
661
+ const first = segs[0];
662
+ const firstBase = segBaseIdx[0];
663
+ emitCap(first, firstBase + 0, firstBase + 1, false, first.posA, first.negA, cap, verts, idx, anchorA, anchorB, anchorT, plA[0], plB[0], plT[0]);
664
+ const last = segs[segs.length - 1];
665
+ const lastBase = segBaseIdx[segs.length - 1];
666
+ const lastSrc = segSrcIdx[segs.length - 1] + 1;
667
+ emitCap(last, lastBase + 2, lastBase + 3, true, last.posB, last.negB, cap, verts, idx, anchorA, anchorB, anchorT, plA[lastSrc], plB[lastSrc], plT[lastSrc]);
668
+ }
669
+ }
670
+ var DEFAULT_MITER_LIMIT = 4;
671
+ var DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD = 1.5;
619
672
  function resolveMarkerSize(ref, strokeWidth, scale = 1) {
620
673
  if (typeof ref === "string" || ref.size === void 0) return strokeWidth;
621
674
  return resolveScreenLength(ref.size, scale);
@@ -718,7 +771,7 @@ function markerHeads(path, stroke, strokeWidth, flattenTolerance, spaces = {}) {
718
771
  want[role] = true;
719
772
  }
720
773
  const out = [];
721
- for (const pl of extractPolylines(path, { flattenTolerance })) {
774
+ for (const pl of extractPolylines(path, strokeFlattening(flattenTolerance, strokeWidth, stroke.vertexWidths))) {
722
775
  for (const [space, want] of groups) {
723
776
  const inSpace = space ? { ...pl, points: pl.points.slice() } : pl;
724
777
  if (space) polylineIntoMetric(inSpace, space.metric);
@@ -782,6 +835,74 @@ function markerReach(stroke) {
782
835
  }
783
836
  return reach;
784
837
  }
838
+ function strokeEdgeNear(path, stroke, x, y, opts = {}) {
839
+ const width = resolveStrokeWidth(stroke.width ?? 1, opts.scale ?? 1);
840
+ const vw = stroke.vertexWidths && stroke.vertexWidths.length > 0 ? stroke.vertexWidths : null;
841
+ const nibScale = stroke.nib ? nibWidthScale(stroke.nib) : null;
842
+ const align = stroke.align ?? "center";
843
+ let best = null;
844
+ for (const pl of extractPolylines(path, strokeFlattening(opts.flattenTolerance, width, vw ?? void 0))) {
845
+ populateWidths(pl, vw, stroke.nib, width);
846
+ const P = pl.points, W = pl.widths, O = pl.offsets;
847
+ const n = P.length / 2;
848
+ const segCount = pl.closed ? n : n - 1;
849
+ const inked = align === "center" ? null : inkedSide(path, P, segCount, align === "inner");
850
+ for (let i = 0; i < segCount; i++) {
851
+ const j = (i + 1) % n;
852
+ const ax = P[i * 2], ay = P[i * 2 + 1];
853
+ const dx = P[j * 2] - ax, dy = P[j * 2 + 1] - ay;
854
+ const len2 = dx * dx + dy * dy;
855
+ if (len2 === 0) continue;
856
+ const len = Math.sqrt(len2);
857
+ const f = Math.min(1, Math.max(0, ((x - ax) * dx + (y - ay) * dy) / len2));
858
+ const fx = ax + dx * f, fy = ay + dy * f;
859
+ const rx = -dy / len, ry = dx / len;
860
+ const d = (x - fx) * rx + (y - fy) * ry;
861
+ const w = W ? W[i] + (W[j] - W[i]) * f : width;
862
+ const o = O ? O[i] + (O[j] - O[i]) * f : 0;
863
+ const side = inked ?? (d >= 0 ? "right" : "left");
864
+ const scale = inked ? 2 : 1;
865
+ const left = inked === "right" ? 0 : scale * (w / 2 - o);
866
+ const right = inked === "left" ? 0 : scale * (w / 2 + o);
867
+ const edge = side === "right" ? right : left;
868
+ const off = Math.hypot(x - fx, y - fy);
869
+ const gap = inked && d >= 0 !== (inked === "right") ? off + edge : off - edge;
870
+ if (best !== null && Math.abs(gap) >= Math.abs(best.gap)) continue;
871
+ const sign = side === "right" ? 1 : -1;
872
+ best = {
873
+ x: fx,
874
+ y: fy,
875
+ nx: rx * sign,
876
+ ny: ry * sign,
877
+ side,
878
+ left,
879
+ right,
880
+ gap,
881
+ factor: scale * (nibScale ? nibScale(dx / len, dy / len) : 1)
882
+ };
883
+ }
884
+ }
885
+ return best;
886
+ }
887
+ function inkedSide(path, P, segCount, inner) {
888
+ const n = P.length / 2;
889
+ let at = -1, longest = 0;
890
+ for (let i = 0; i < segCount; i++) {
891
+ const j2 = (i + 1) % n;
892
+ const l = Math.hypot(P[j2 * 2] - P[i * 2], P[j2 * 2 + 1] - P[i * 2 + 1]);
893
+ if (l > longest) {
894
+ longest = l;
895
+ at = i;
896
+ }
897
+ }
898
+ if (at < 0) return inner ? "right" : "left";
899
+ const j = (at + 1) % n;
900
+ const dx = P[j * 2] - P[at * 2], dy = P[j * 2 + 1] - P[at * 2 + 1];
901
+ const probe = Math.min(longest, 1) * 1e-3;
902
+ const mx = (P[at * 2] + P[j * 2]) / 2 - dy / longest * probe;
903
+ const my = (P[at * 2 + 1] + P[j * 2 + 1]) / 2 + dx / longest * probe;
904
+ return pointInPath(path, mx, my) === inner ? "right" : "left";
905
+ }
785
906
  var EMPTY = {
786
907
  kind: "polygon",
787
908
  commands: new Uint8Array(0),
@@ -803,7 +924,7 @@ function outlineOf(path, stroke, opts) {
803
924
  const insets = strokeInsets(sized, width);
804
925
  const align = stroke.align ?? "center";
805
926
  const cut = align !== "center" && path.kind !== "rect";
806
- const ribbonStroke = cut ? { ...sized, width: width * 2, align: "center", vertexWidths: stroke.vertexWidths?.map((w) => w * 2) } : sized;
927
+ const ribbonStroke = cut ? { ...sized, width: width * 2, align: "center", vertexWidths: stroke.vertexWidths?.map((w) => scaleVertexWidth(w, 2)) } : sized;
807
928
  const mesh = tessellateStroke(path, ribbonStroke, {
808
929
  flattenTolerance: tolerance,
809
930
  startInset: insets.start,
@@ -820,7 +941,31 @@ function outlineOf(path, stroke, opts) {
820
941
  }
821
942
  return heads.length === 0 ? ribbon : pathUnion(ribbon, ...heads);
822
943
  }
944
+ function offsetPath(path, distance, opts = {}) {
945
+ if (distance === 0) return closedRegion(path);
946
+ const region = closedRegion(resolveCorners(path));
947
+ const tolerance = opts.tolerance ?? 0.1;
948
+ const band = strokeToPath(region, {
949
+ width: Math.abs(distance) * 2,
950
+ join: opts.join ?? "miter",
951
+ miterLimit: opts.miterLimit ?? 4
952
+ }, { flattenTolerance: tolerance });
953
+ const cut = distance > 0 ? pathUnion(region, band) : pathSubtract(region, band);
954
+ const corners = offsetCornerTraces(path, distance);
955
+ const grown = restoreCorners(cut, corners, tolerance);
956
+ if (opts.refit === false || grown.commands.length === 0) return grown;
957
+ return restoreCorners(simplifyPath(grown, tolerance), corners, tolerance);
958
+ }
959
+ function closedRegion(path) {
960
+ if (path.kind === "rect") {
961
+ const { x, y, width: w, height: h } = path;
962
+ return anchorsToPath([[{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }]], [true]);
963
+ }
964
+ const { anchors, closed } = pathToAnchors(path);
965
+ if (closed.every(Boolean)) return path;
966
+ return anchorsToPath(anchors, closed.map(() => true), path);
967
+ }
823
968
 
824
- export { 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 };
969
+ export { coordsOutOfMetric, 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 };
825
970
  //# sourceMappingURL=index.js.map
826
971
  //# sourceMappingURL=index.js.map