@weasel-js/stroke 1.10.1 → 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,160 +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, simplifyPath, anchorsToPath, pathToAnchors, DEFAULT_FLATTEN_TOLERANCE, 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;
151
133
  }
152
- var STROKE_MAX_TURN = Math.PI / 18;
153
- function strokeFlattening(flattenTolerance, width, vertexWidths) {
154
- const tolerance = flattenTolerance ?? DEFAULT_FLATTEN_TOLERANCE;
155
- let widest = width;
156
- if (vertexWidths) for (const w of vertexWidths) widest = Math.max(widest, w);
157
- return widest / 2 > tolerance ? { flattenTolerance, maxTurn: STROKE_MAX_TURN } : { flattenTolerance };
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);
158
183
  }
159
184
 
160
185
  // src/tessellate/vertexWidths.ts
@@ -165,23 +190,36 @@ function populatePolylineWidths(pl, vertexWidths, fallbackWidth, scale) {
165
190
  const aB = pl.anchorB ?? new Uint32Array(ptCount);
166
191
  const aT = pl.anchorT ?? new Float32Array(ptCount);
167
192
  const widths = new Float32Array(ptCount);
193
+ const offsets = new Float32Array(ptCount);
194
+ let sided = false;
195
+ const fallback = fallbackWidth / 2;
168
196
  let k = 1;
169
- const read = (i) => {
170
- const v = vertexWidths[i];
171
- 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];
172
200
  };
173
201
  for (let i = 0; i < ptCount; i++) {
174
202
  if (scale) k = scaleAt(pl, i, scale);
175
203
  const a = aA[i], b = aB[i], t = aT[i];
176
- if (a === b) {
177
- widths[i] = read(a);
178
- } else {
179
- const wa = read(a);
180
- const wb = read(b);
181
- 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;
182
209
  }
210
+ widths[i] = left + right;
211
+ offsets[i] = (right - left) / 2;
212
+ if (offsets[i] !== 0) sided = true;
183
213
  }
184
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);
185
223
  }
186
224
  function scaleAt(pl, i, scale) {
187
225
  const pts = pl.points;
@@ -203,6 +241,14 @@ function scaleAt(pl, i, scale) {
203
241
  return len > 0 ? scale(tx / len, ty / len) : 1;
204
242
  }
205
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
+
206
252
  // src/tessellate/metric.ts
207
253
  function screenSpaceKey(space) {
208
254
  return space === void 0 ? "" : `${metricKey(space.metric)}@${space.pxPerUnit}`;
@@ -297,143 +343,16 @@ function trimInSpace(pl, from, ribbon, start, end) {
297
343
  return cur;
298
344
  }
299
345
 
300
- // src/tessellate/stroke.ts
301
- var EMPTY_MESH = {
302
- vertices: new Float32Array(0),
303
- indices: new Uint32Array(0),
304
- anchorA: new Uint32Array(0),
305
- anchorB: new Uint32Array(0),
306
- anchorT: new Float32Array(0)
307
- };
308
- function resolveStrokeWidth(width, scale) {
309
- return resolveScreenLength(width, scale);
310
- }
311
- function tessellateStroke(path, stroke, opts = {}) {
312
- const width = resolveStrokeWidth(stroke.width ?? 1, 1);
313
- if (width <= 0) return EMPTY_MESH;
314
- const join = stroke.join ?? "miter";
315
- const cap = stroke.cap ?? "butt";
316
- const align = stroke.align ?? "center";
317
- const miterLimit = stroke.miterLimit ?? DEFAULT_MITER_LIMIT;
318
- const varyingThreshold = stroke.varyingWidthJoinThreshold ?? DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD;
319
- const metric = opts.metric;
320
- const rectInMetric = metric && path.kind === "rect" ? rectIntoMetric(path, metric) : null;
321
- let workingPath = rectInMetric ?? path;
322
- if (workingPath.kind === "rect" && align !== "center") {
323
- workingPath = { kind: "rect", ...alignedStrokeRect(workingPath, align, width) };
324
- }
325
- const vertexWidths = stroke.vertexWidths && stroke.vertexWidths.length > 0 ? stroke.vertexWidths : null;
326
- const polylines = extractPolylines(workingPath, strokeFlattening(opts.flattenTolerance, width, vertexWidths ?? void 0));
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);
@@ -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,
@@ -821,16 +942,19 @@ function outlineOf(path, stroke, opts) {
821
942
  return heads.length === 0 ? ribbon : pathUnion(ribbon, ...heads);
822
943
  }
823
944
  function offsetPath(path, distance, opts = {}) {
824
- const region = closedRegion(path);
825
- if (distance === 0) return region;
945
+ if (distance === 0) return closedRegion(path);
946
+ const region = closedRegion(resolveCorners(path));
826
947
  const tolerance = opts.tolerance ?? 0.1;
827
948
  const band = strokeToPath(region, {
828
949
  width: Math.abs(distance) * 2,
829
950
  join: opts.join ?? "miter",
830
951
  miterLimit: opts.miterLimit ?? 4
831
952
  }, { flattenTolerance: tolerance });
832
- const grown = distance > 0 ? pathUnion(region, band) : pathSubtract(region, band);
833
- return opts.refit === false || grown.commands.length === 0 ? grown : simplifyPath(grown, 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);
834
958
  }
835
959
  function closedRegion(path) {
836
960
  if (path.kind === "rect") {
@@ -842,6 +966,6 @@ function closedRegion(path) {
842
966
  return anchorsToPath(anchors, closed.map(() => true), path);
843
967
  }
844
968
 
845
- export { coordsOutOfMetric, markerHeads, markerInset, markerKeyOf, markerReach, markerReadsKey, markerSites, metricKeepsRects, metricKey, offsetPath, 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 };
846
970
  //# sourceMappingURL=index.js.map
847
971
  //# sourceMappingURL=index.js.map