@motionscript/renderer 0.1.0-alpha.9 → 0.1.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
@@ -8242,6 +8242,222 @@ function dashedPath(cs, dash, offset, dots, fit) {
8242
8242
  return any ? b.build() : null;
8243
8243
  }
8244
8244
  //#endregion
8245
+ //#region web/src/adapter/svg-path.ts
8246
+ /** Endpoint-parameterised arc → cubic segments (SVG implementation notes F.6). */
8247
+ function arcTo(b, x1, y1, rx, ry, phi, large, sweep, x2, y2) {
8248
+ if (rx === 0 || ry === 0) {
8249
+ b.lineTo(x2, y2);
8250
+ return;
8251
+ }
8252
+ rx = Math.abs(rx);
8253
+ ry = Math.abs(ry);
8254
+ const c = Math.cos(phi), s = Math.sin(phi);
8255
+ const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
8256
+ const xp = c * dx + s * dy, yp = -s * dx + c * dy;
8257
+ const lam = xp * xp / (rx * rx) + yp * yp / (ry * ry);
8258
+ if (lam > 1) {
8259
+ rx *= Math.sqrt(lam);
8260
+ ry *= Math.sqrt(lam);
8261
+ }
8262
+ const num = rx * rx * ry * ry - rx * rx * yp * yp - ry * ry * xp * xp;
8263
+ const den = rx * rx * yp * yp + ry * ry * xp * xp;
8264
+ let k = Math.sqrt(Math.max(0, num / den));
8265
+ if (large === sweep) k = -k;
8266
+ const cxp = k * rx * yp / ry, cyp = -k * ry * xp / rx;
8267
+ const cx = c * cxp - s * cyp + (x1 + x2) / 2, cy = s * cxp + c * cyp + (y1 + y2) / 2;
8268
+ const ang = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
8269
+ const t1 = ang(1, 0, (xp - cxp) / rx, (yp - cyp) / ry);
8270
+ let dt = ang((xp - cxp) / rx, (yp - cyp) / ry, (-xp - cxp) / rx, (-yp - cyp) / ry);
8271
+ if (!sweep && dt > 0) dt -= 2 * Math.PI;
8272
+ else if (sweep && dt < 0) dt += 2 * Math.PI;
8273
+ const n = Math.ceil(Math.abs(dt) / (Math.PI / 2) - 1e-9);
8274
+ const seg = dt / n;
8275
+ const alpha = 4 / 3 * Math.tan(seg / 4);
8276
+ for (let i = 0; i < n; i++) {
8277
+ const ta = t1 + i * seg, tb = ta + seg;
8278
+ const cosA = Math.cos(ta), sinA = Math.sin(ta), cosB = Math.cos(tb), sinB = Math.sin(tb);
8279
+ const ax = cx + rx * cosA * c - ry * sinA * s, ay = cy + rx * cosA * s + ry * sinA * c;
8280
+ const bx = cx + rx * cosB * c - ry * sinB * s, by = cy + rx * cosB * s + ry * sinB * c;
8281
+ const dax = -rx * sinA * c - ry * cosA * s, day = -rx * sinA * s + ry * cosA * c;
8282
+ const dbx = -rx * sinB * c - ry * cosB * s, dby = -rx * sinB * s + ry * cosB * c;
8283
+ b.cubicTo(ax + alpha * dax, ay + alpha * day, bx - alpha * dbx, by - alpha * dby, i === n - 1 ? x2 : bx, i === n - 1 ? y2 : by);
8284
+ }
8285
+ }
8286
+ function parseSvgPath(d) {
8287
+ const b = new PathBuilder();
8288
+ const tokens = d.match(/[a-zA-Z]|-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g) ?? [];
8289
+ let i = 0;
8290
+ let cmd = "";
8291
+ let x = 0, y = 0, sx = 0, sy = 0;
8292
+ let lcx = 0, lcy = 0, lqx = 0, lqy = 0;
8293
+ let prev = "";
8294
+ const num = () => Number(tokens[i++]);
8295
+ const more = () => i < tokens.length && !/[a-zA-Z]/.test(tokens[i]);
8296
+ while (i < tokens.length) {
8297
+ if (/[a-zA-Z]/.test(tokens[i])) cmd = tokens[i++];
8298
+ const rel = cmd === cmd.toLowerCase();
8299
+ const C = cmd.toUpperCase();
8300
+ const ox = rel ? x : 0, oy = rel ? y : 0;
8301
+ switch (C) {
8302
+ case "M":
8303
+ x = ox + num();
8304
+ y = oy + num();
8305
+ b.moveTo(x, y);
8306
+ sx = x;
8307
+ sy = y;
8308
+ cmd = rel ? "l" : "L";
8309
+ break;
8310
+ case "L":
8311
+ x = ox + num();
8312
+ y = oy + num();
8313
+ b.lineTo(x, y);
8314
+ break;
8315
+ case "H":
8316
+ x = (rel ? x : 0) + num();
8317
+ b.lineTo(x, y);
8318
+ break;
8319
+ case "V":
8320
+ y = (rel ? y : 0) + num();
8321
+ b.lineTo(x, y);
8322
+ break;
8323
+ case "C": {
8324
+ const x1 = ox + num(), y1 = oy + num(), x2 = ox + num(), y2 = oy + num();
8325
+ x = ox + num();
8326
+ y = oy + num();
8327
+ b.cubicTo(x1, y1, x2, y2, x, y);
8328
+ lcx = x2;
8329
+ lcy = y2;
8330
+ break;
8331
+ }
8332
+ case "S": {
8333
+ const r = prev === "C" || prev === "S";
8334
+ const x1 = r ? 2 * x - lcx : x, y1 = r ? 2 * y - lcy : y;
8335
+ const x2 = ox + num(), y2 = oy + num();
8336
+ x = ox + num();
8337
+ y = oy + num();
8338
+ b.cubicTo(x1, y1, x2, y2, x, y);
8339
+ lcx = x2;
8340
+ lcy = y2;
8341
+ break;
8342
+ }
8343
+ case "Q":
8344
+ lqx = ox + num();
8345
+ lqy = oy + num();
8346
+ x = ox + num();
8347
+ y = oy + num();
8348
+ b.quadTo(lqx, lqy, x, y);
8349
+ break;
8350
+ case "T": {
8351
+ const r = prev === "Q" || prev === "T";
8352
+ lqx = r ? 2 * x - lqx : x;
8353
+ lqy = r ? 2 * y - lqy : y;
8354
+ x = ox + num();
8355
+ y = oy + num();
8356
+ b.quadTo(lqx, lqy, x, y);
8357
+ break;
8358
+ }
8359
+ case "A": {
8360
+ const rx = num(), ry = num(), phi = num() * Math.PI / 180, large = num() !== 0, sweep = num() !== 0;
8361
+ const nx = ox + num(), ny = oy + num();
8362
+ arcTo(b, x, y, rx, ry, phi, large, sweep, nx, ny);
8363
+ x = nx;
8364
+ y = ny;
8365
+ break;
8366
+ }
8367
+ case "Z":
8368
+ b.close();
8369
+ x = sx;
8370
+ y = sy;
8371
+ break;
8372
+ default: i++;
8373
+ }
8374
+ prev = C;
8375
+ if (C === "Z" && more()) cmd = "L";
8376
+ }
8377
+ return b.build();
8378
+ }
8379
+ //#endregion
8380
+ //#region web/src/adapter/geometry/sink.ts
8381
+ var SvgSink = class {
8382
+ parts = [];
8383
+ move(x, y) {
8384
+ this.parts.push(`M ${x} ${y}`);
8385
+ }
8386
+ line(x, y) {
8387
+ this.parts.push(`L ${x} ${y}`);
8388
+ }
8389
+ arc(rx, ry, large, sweep, x, y) {
8390
+ this.parts.push(`A ${rx} ${ry} 0 ${large} ${sweep} ${x} ${y}`);
8391
+ }
8392
+ close() {
8393
+ this.parts.push("Z");
8394
+ }
8395
+ toString() {
8396
+ return this.parts.join(" ");
8397
+ }
8398
+ };
8399
+ /**
8400
+ * The outline as `parseSvgPath` reads it from {@link SvgSink}'s string, without
8401
+ * the string. A finite number prints and parses back to itself, and the parser
8402
+ * adds an absolute coordinate to 0 (so −0 lands as 0); a number that is not
8403
+ * finite does not survive the tokenizer, so `ok` turns false and the caller
8404
+ * takes the string route.
8405
+ */
8406
+ var PathDataSink = class {
8407
+ ok = true;
8408
+ b = new PathBuilder();
8409
+ empty = true;
8410
+ x = 0;
8411
+ y = 0;
8412
+ sx = 0;
8413
+ sy = 0;
8414
+ move(x, y) {
8415
+ this.check(x, y);
8416
+ this.x = this.sx = 0 + x;
8417
+ this.y = this.sy = 0 + y;
8418
+ this.b.moveTo(this.x, this.y);
8419
+ }
8420
+ line(x, y) {
8421
+ this.check(x, y);
8422
+ this.x = 0 + x;
8423
+ this.y = 0 + y;
8424
+ this.b.lineTo(this.x, this.y);
8425
+ }
8426
+ arc(rx, ry, large, sweep, x, y) {
8427
+ this.check(rx, ry, x, y);
8428
+ const nx = 0 + x, ny = 0 + y;
8429
+ arcTo(this.b, this.x, this.y, rx + 0, ry + 0, 0, large !== 0, sweep !== 0, nx, ny);
8430
+ this.x = nx;
8431
+ this.y = ny;
8432
+ }
8433
+ close() {
8434
+ this.empty = false;
8435
+ this.b.close();
8436
+ this.x = this.sx;
8437
+ this.y = this.sy;
8438
+ }
8439
+ result() {
8440
+ return this.empty ? null : this.b.build();
8441
+ }
8442
+ check(...values) {
8443
+ this.empty = false;
8444
+ for (const v of values) if (!Number.isFinite(v)) this.ok = false;
8445
+ }
8446
+ };
8447
+ function svgOf(emit) {
8448
+ const sink = new SvgSink();
8449
+ emit(sink);
8450
+ return sink.toString();
8451
+ }
8452
+ /** What `parseSvgPath(svgOf(emit))` gives (null for an empty string), built directly. */
8453
+ function pathDataOf(emit) {
8454
+ const sink = new PathDataSink();
8455
+ emit(sink);
8456
+ if (sink.ok) return sink.result();
8457
+ const svg = svgOf(emit);
8458
+ return svg ? parseSvgPath(svg) : null;
8459
+ }
8460
+ //#endregion
8245
8461
  //#region web/src/adapter/geometry/base.ts
8246
8462
  var ShapeGeometry = class {
8247
8463
  fullState;
@@ -8249,6 +8465,11 @@ var ShapeGeometry = class {
8249
8465
  constructor(state) {
8250
8466
  this.fullState = this.resolveState(state);
8251
8467
  }
8468
+ /** `buildSVGPath` parsed, or null where it is empty. A shape writing to a {@link PathSink} skips the string. */
8469
+ buildPathData(geo) {
8470
+ const svg = this.buildSVGPath(geo);
8471
+ return svg ? parseSvgPath(svg) : null;
8472
+ }
8252
8473
  get geometry() {
8253
8474
  return this.geo ??= this.computeGeometry();
8254
8475
  }
@@ -8287,24 +8508,29 @@ function cornerReach(c) {
8287
8508
  return c.radius > 0 ? c.radius : 0;
8288
8509
  }
8289
8510
  /**
8290
- * SVG commands for a right-angle (rect) corner, the pen already at its entry
8291
- * point `apex + d0·radius`; they leave it at `apex + d1·radius`. `d0` points
8511
+ * A right-angle (rect) corner, the pen already at its entry point
8512
+ * `apex + d0·radius`; it leaves the pen at `apex + d1·radius`. `d0` points
8292
8513
  * from the apex back along the incoming edge, `d1` along the outgoing one.
8293
- * Empty for a zero radius (the edge runs on to a sharp apex).
8514
+ * Nothing for a zero radius (the edge runs on to a sharp apex).
8294
8515
  */
8295
- function cornerCommands(ax, ay, d0x, d0y, d1x, d1y, c) {
8296
- if (c.radius <= 0) return "";
8297
- return cornerCommandsFromCut(ax, ay, d0x, d0y, d1x, d1y, c.radius, c.radius, c.style);
8516
+ function cornerTo(sink, ax, ay, d0x, d0y, d1x, d1y, c) {
8517
+ if (c.radius <= 0) return;
8518
+ cutCornerTo(sink, ax, ay, d0x, d0y, d1x, d1y, c.radius, c.radius, c.style);
8298
8519
  }
8299
8520
  /**
8300
- * SVG commands for a corner of any angle, given the cut-back distance `cut`
8301
- * along each edge and the arc `radius` (they differ off a right angle). The pen
8302
- * is at `apex + d0·cut`; the commands leave it at `apex + d1·cut`.
8521
+ * A corner of any angle, given the cut-back distance `cut` along each edge and
8522
+ * the arc `radius` (they differ off a right angle). The pen is at
8523
+ * `apex + d0·cut`; it is left at `apex + d1·cut`.
8303
8524
  */
8304
- function cornerCommandsFromCut(ax, ay, d0x, d0y, d1x, d1y, cut, radius, style) {
8525
+ function cutCornerTo(sink, ax, ay, d0x, d0y, d1x, d1y, cut, radius, style) {
8305
8526
  const ex = ax + d1x * cut, ey = ay + d1y * cut;
8306
- if (style === "angled") return `L ${ex} ${ey}`;
8307
- return `A ${radius} ${radius} 0 0 ${d0x * d1y - d0y * d1x < 0 ? 1 : 0} ${ex} ${ey}`;
8527
+ if (style === "angled") return sink.line(ex, ey);
8528
+ const cross = d0x * d1y - d0y * d1x;
8529
+ sink.arc(radius, radius, 0, cross < 0 ? 1 : 0, ex, ey);
8530
+ }
8531
+ /** {@link cutCornerTo} as SVG commands. */
8532
+ function cornerCommandsFromCut(ax, ay, d0x, d0y, d1x, d1y, cut, radius, style) {
8533
+ return svgOf((sink) => cutCornerTo(sink, ax, ay, d0x, d0y, d1x, d1y, cut, radius, style));
8308
8534
  }
8309
8535
  //#endregion
8310
8536
  //#region web/src/adapter/geometry/ellipse.ts
@@ -8333,21 +8559,19 @@ var EllipseGeometry = class extends ShapeGeometry {
8333
8559
  };
8334
8560
  }
8335
8561
  buildSVGPath(geo) {
8336
- const { cx, cy, halfWidth, halfHeight, ratio } = geo;
8337
- if (ratio >= 1) return "";
8338
- if (!geo.isFullShape) return ratio <= 0 ? wedge(geo) : annularSector(geo);
8339
- const outer = fullEllipse(cx, cy, halfWidth, halfHeight, 0);
8340
- if (ratio <= 0) return outer;
8341
- return `${outer} ${fullEllipse(cx, cy, halfWidth * ratio, halfHeight * ratio, 1)}`;
8562
+ return svgOf((sink) => outline$1(sink, geo));
8563
+ }
8564
+ buildPathData(geo) {
8565
+ return pathDataOf((sink) => outline$1(sink, geo));
8342
8566
  }
8343
8567
  /** What a ratio-1 ellipse strokes, once: the bare outline or open arc. Null below ratio 1. */
8344
8568
  ringOutline(geo) {
8345
8569
  if (geo.ratio < 1) return null;
8346
- return geo.isFullShape ? fullEllipse(geo.cx, geo.cy, geo.halfWidth, geo.halfHeight, 0) : openArc(geo);
8570
+ return svgOf((sink) => geo.isFullShape ? fullEllipse(sink, geo.cx, geo.cy, geo.halfWidth, geo.halfHeight, 0) : openArc(sink, geo));
8347
8571
  }
8348
8572
  buildSVGAlignInterior(geo) {
8349
- if (geo.isFullShape) return geo.ratio >= 1 ? fullEllipse(geo.cx, geo.cy, geo.halfWidth, geo.halfHeight, 0) : null;
8350
- return geo.ratio >= 1 || geo.ratio <= 0 ? wedge(geo) : annularSector(geo);
8573
+ if (geo.isFullShape) return geo.ratio >= 1 ? svgOf((sink) => fullEllipse(sink, geo.cx, geo.cy, geo.halfWidth, geo.halfHeight, 0)) : null;
8574
+ return svgOf((sink) => geo.ratio >= 1 || geo.ratio <= 0 ? wedge(sink, geo) : annularSector(sink, geo));
8351
8575
  }
8352
8576
  supportsSpread() {
8353
8577
  return true;
@@ -8356,7 +8580,7 @@ var EllipseGeometry = class extends ShapeGeometry {
8356
8580
  if (!geo.isFullShape || geo.ratio >= 1) return null;
8357
8581
  const halfWidth = geo.halfWidth + spread, halfHeight = geo.halfHeight + spread;
8358
8582
  if (halfWidth <= 0 || halfHeight <= 0) return null;
8359
- return fullEllipse(geo.cx, geo.cy, halfWidth, halfHeight, 0);
8583
+ return svgOf((sink) => fullEllipse(sink, geo.cx, geo.cy, halfWidth, halfHeight, 0));
8360
8584
  }
8361
8585
  computeBounds(geo) {
8362
8586
  const { cx, cy, halfWidth, halfHeight } = geo;
@@ -8368,11 +8592,21 @@ var EllipseGeometry = class extends ShapeGeometry {
8368
8592
  };
8369
8593
  }
8370
8594
  };
8371
- function fullEllipse(cx, cy, rx, ry, sweepFlag) {
8372
- const left = cx - rx;
8373
- return `M ${left} ${cy} A ${rx} ${ry} 0 1 ${sweepFlag} ${cx + rx} ${cy} A ${rx} ${ry} 0 1 ${sweepFlag} ${left} ${cy} Z`;
8374
- }
8375
- /** A partial sweep's outer curve: its start point, and the `A` that runs it round. */
8595
+ function outline$1(sink, geo) {
8596
+ const { cx, cy, halfWidth, halfHeight, ratio } = geo;
8597
+ if (ratio >= 1) return;
8598
+ if (!geo.isFullShape) return ratio <= 0 ? wedge(sink, geo) : annularSector(sink, geo);
8599
+ fullEllipse(sink, cx, cy, halfWidth, halfHeight, 0);
8600
+ if (ratio > 0) fullEllipse(sink, cx, cy, halfWidth * ratio, halfHeight * ratio, 1);
8601
+ }
8602
+ function fullEllipse(sink, cx, cy, rx, ry, sweepFlag) {
8603
+ const left = cx - rx, right = cx + rx;
8604
+ sink.move(left, cy);
8605
+ sink.arc(rx, ry, 1, sweepFlag, right, cy);
8606
+ sink.arc(rx, ry, 1, sweepFlag, left, cy);
8607
+ sink.close();
8608
+ }
8609
+ /** A partial sweep's outer curve: its start point, and the arc that runs it round. */
8376
8610
  function outerArc(geo) {
8377
8611
  const { cx, cy, halfWidth, halfHeight, startAngle, sweep } = geo;
8378
8612
  const endAngle = startAngle + sweep;
@@ -8381,26 +8615,38 @@ function outerArc(geo) {
8381
8615
  const sx = cx + halfWidth * Math.cos(startAngle * TO_RAD), sy = cy + halfHeight * Math.sin(startAngle * TO_RAD);
8382
8616
  const ex = cx + halfWidth * Math.cos(endAngle * TO_RAD), ey = cy + halfHeight * Math.sin(endAngle * TO_RAD);
8383
8617
  return {
8384
- start: `${sx} ${sy}`,
8385
- arc: `A ${halfWidth} ${halfHeight} 0 ${largeArc} ${sweepFlag} ${ex} ${ey}`
8618
+ sx,
8619
+ sy,
8620
+ arc: (sink) => sink.arc(halfWidth, halfHeight, largeArc, sweepFlag, ex, ey)
8386
8621
  };
8387
8622
  }
8388
- function openArc(geo) {
8389
- const { start, arc } = outerArc(geo);
8390
- return `M ${start} ${arc}`;
8623
+ function openArc(sink, geo) {
8624
+ const { sx, sy, arc } = outerArc(geo);
8625
+ sink.move(sx, sy);
8626
+ arc(sink);
8391
8627
  }
8392
- function wedge(geo) {
8393
- const { start, arc } = outerArc(geo);
8394
- return `M ${geo.cx} ${geo.cy} L ${start} ${arc} Z`;
8628
+ function wedge(sink, geo) {
8629
+ const { sx, sy, arc } = outerArc(geo);
8630
+ sink.move(geo.cx, geo.cy);
8631
+ sink.line(sx, sy);
8632
+ arc(sink);
8633
+ sink.close();
8395
8634
  }
8396
8635
  /** Out along the outer arc, in to the inner edge, back along the inner arc, closed along the start radius. */
8397
- function annularSector(geo) {
8636
+ function annularSector(sink, geo) {
8398
8637
  const { cx, cy, halfWidth, halfHeight, startAngle, sweep, ratio } = geo;
8399
- const { start, arc } = outerArc(geo);
8638
+ const { sx, sy, arc } = outerArc(geo);
8400
8639
  const endAngle = startAngle + sweep;
8401
8640
  const innerW = halfWidth * ratio, innerH = halfHeight * ratio;
8402
8641
  const isx = cx + innerW * Math.cos(startAngle * TO_RAD), isy = cy + innerH * Math.sin(startAngle * TO_RAD);
8403
- return `M ${start} ${arc} L ${cx + innerW * Math.cos(endAngle * TO_RAD)} ${cy + innerH * Math.sin(endAngle * TO_RAD)} A ${innerW} ${innerH} 0 ${Math.abs(sweep) > 180 ? 1 : 0} ${sweep > 0 ? 0 : 1} ${isx} ${isy} Z`;
8642
+ const iex = cx + innerW * Math.cos(endAngle * TO_RAD), iey = cy + innerH * Math.sin(endAngle * TO_RAD);
8643
+ const largeArc = Math.abs(sweep) > 180 ? 1 : 0;
8644
+ const innerSweepFlag = sweep > 0 ? 0 : 1;
8645
+ sink.move(sx, sy);
8646
+ arc(sink);
8647
+ sink.line(iex, iey);
8648
+ sink.arc(innerW, innerH, largeArc, innerSweepFlag, isx, isy);
8649
+ sink.close();
8404
8650
  }
8405
8651
  //#endregion
8406
8652
  //#region web/src/adapter/geometry/line.ts
@@ -8596,22 +8842,31 @@ var PolygramGeometry = class extends ShapeGeometry {
8596
8842
  //#endregion
8597
8843
  //#region web/src/adapter/geometry/rect.ts
8598
8844
  /** The outline clockwise, each corner its own arc or chamfer `radius` px from the apex. */
8599
- function rectToSvg(l, t, r, b, tl, tr, br, bl) {
8845
+ function roundedRect(sink, l, t, r, b, tl, tr, br, bl) {
8600
8846
  const rTL = cornerReach(tl), rTR = cornerReach(tr), rBR = cornerReach(br), rBL = cornerReach(bl);
8601
- return [
8602
- `M ${l + rTL} ${t}`,
8603
- `L ${r - rTR} ${t}`,
8604
- cornerCommands(r, t, -1, 0, 0, 1, tr),
8605
- `L ${r} ${b - rBR}`,
8606
- cornerCommands(r, b, 0, -1, -1, 0, br),
8607
- `L ${l + rBL} ${b}`,
8608
- cornerCommands(l, b, 1, 0, 0, -1, bl),
8609
- `L ${l} ${t + rTL}`,
8610
- cornerCommands(l, t, 0, 1, 1, 0, tl),
8611
- "Z"
8612
- ].filter(Boolean).join(" ");
8613
- }
8614
- var plainRect = (l, t, r, b) => `M ${l} ${t} L ${r} ${t} L ${r} ${b} L ${l} ${b} Z`;
8847
+ sink.move(l + rTL, t);
8848
+ sink.line(r - rTR, t);
8849
+ cornerTo(sink, r, t, -1, 0, 0, 1, tr);
8850
+ sink.line(r, b - rBR);
8851
+ cornerTo(sink, r, b, 0, -1, -1, 0, br);
8852
+ sink.line(l + rBL, b);
8853
+ cornerTo(sink, l, b, 1, 0, 0, -1, bl);
8854
+ sink.line(l, t + rTL);
8855
+ cornerTo(sink, l, t, 0, 1, 1, 0, tl);
8856
+ sink.close();
8857
+ }
8858
+ function plainRect(sink, l, t, r, b) {
8859
+ sink.move(l, t);
8860
+ sink.line(r, t);
8861
+ sink.line(r, b);
8862
+ sink.line(l, b);
8863
+ sink.close();
8864
+ }
8865
+ function outline(sink, geo) {
8866
+ const { left, top, right, bottom, tl, tr, br, bl, isZero } = geo;
8867
+ if (isZero) plainRect(sink, left, top, right, bottom);
8868
+ else roundedRect(sink, left, top, right, bottom, tl, tr, br, bl);
8869
+ }
8615
8870
  /** Per-corner radii and styles, scaled down together where adjacent corners would overrun a side. */
8616
8871
  var RectGeometry = class extends ShapeGeometry {
8617
8872
  resolveState(state) {
@@ -8662,8 +8917,10 @@ var RectGeometry = class extends ShapeGeometry {
8662
8917
  };
8663
8918
  }
8664
8919
  buildSVGPath(geo) {
8665
- const { left, top, right, bottom, tl, tr, br, bl, isZero } = geo;
8666
- return isZero ? plainRect(left, top, right, bottom) : rectToSvg(left, top, right, bottom, tl, tr, br, bl);
8920
+ return svgOf((sink) => outline(sink, geo));
8921
+ }
8922
+ buildPathData(geo) {
8923
+ return pathDataOf((sink) => outline(sink, geo));
8667
8924
  }
8668
8925
  supportsSpread() {
8669
8926
  return true;
@@ -8694,8 +8951,8 @@ var RectGeometry = class extends ShapeGeometry {
8694
8951
  tr.radius *= scale;
8695
8952
  br.radius *= scale;
8696
8953
  bl.radius *= scale;
8697
- if (tl.radius === 0 && tr.radius === 0 && br.radius === 0 && bl.radius === 0) return plainRect(left, top, right, bottom);
8698
- return rectToSvg(left, top, right, bottom, tl, tr, br, bl);
8954
+ if (tl.radius === 0 && tr.radius === 0 && br.radius === 0 && bl.radius === 0) return svgOf((sink) => plainRect(sink, left, top, right, bottom));
8955
+ return svgOf((sink) => roundedRect(sink, left, top, right, bottom, tl, tr, br, bl));
8699
8956
  }
8700
8957
  computeBounds(geo) {
8701
8958
  return {
@@ -8716,139 +8973,6 @@ function cornerScale(w, h, tl, tr, br, bl) {
8716
8973
  return Math.min(top, bottom, left, right);
8717
8974
  }
8718
8975
  //#endregion
8719
- //#region web/src/adapter/svg-path.ts
8720
- /** Endpoint-parameterised arc → cubic segments (SVG implementation notes F.6). */
8721
- function arcTo(b, x1, y1, rx, ry, phi, large, sweep, x2, y2) {
8722
- if (rx === 0 || ry === 0) {
8723
- b.lineTo(x2, y2);
8724
- return;
8725
- }
8726
- rx = Math.abs(rx);
8727
- ry = Math.abs(ry);
8728
- const c = Math.cos(phi), s = Math.sin(phi);
8729
- const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
8730
- const xp = c * dx + s * dy, yp = -s * dx + c * dy;
8731
- const lam = xp * xp / (rx * rx) + yp * yp / (ry * ry);
8732
- if (lam > 1) {
8733
- rx *= Math.sqrt(lam);
8734
- ry *= Math.sqrt(lam);
8735
- }
8736
- const num = rx * rx * ry * ry - rx * rx * yp * yp - ry * ry * xp * xp;
8737
- const den = rx * rx * yp * yp + ry * ry * xp * xp;
8738
- let k = Math.sqrt(Math.max(0, num / den));
8739
- if (large === sweep) k = -k;
8740
- const cxp = k * rx * yp / ry, cyp = -k * ry * xp / rx;
8741
- const cx = c * cxp - s * cyp + (x1 + x2) / 2, cy = s * cxp + c * cyp + (y1 + y2) / 2;
8742
- const ang = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
8743
- const t1 = ang(1, 0, (xp - cxp) / rx, (yp - cyp) / ry);
8744
- let dt = ang((xp - cxp) / rx, (yp - cyp) / ry, (-xp - cxp) / rx, (-yp - cyp) / ry);
8745
- if (!sweep && dt > 0) dt -= 2 * Math.PI;
8746
- else if (sweep && dt < 0) dt += 2 * Math.PI;
8747
- const n = Math.ceil(Math.abs(dt) / (Math.PI / 2) - 1e-9);
8748
- const seg = dt / n;
8749
- const alpha = 4 / 3 * Math.tan(seg / 4);
8750
- const pt = (t) => [cx + rx * Math.cos(t) * c - ry * Math.sin(t) * s, cy + rx * Math.cos(t) * s + ry * Math.sin(t) * c];
8751
- const dpt = (t) => [-rx * Math.sin(t) * c - ry * Math.cos(t) * s, -rx * Math.sin(t) * s + ry * Math.cos(t) * c];
8752
- for (let i = 0; i < n; i++) {
8753
- const ta = t1 + i * seg, tb = ta + seg;
8754
- const [ax, ay] = pt(ta), [bx, by] = pt(tb), [dax, day] = dpt(ta), [dbx, dby] = dpt(tb);
8755
- b.cubicTo(ax + alpha * dax, ay + alpha * day, bx - alpha * dbx, by - alpha * dby, i === n - 1 ? x2 : bx, i === n - 1 ? y2 : by);
8756
- }
8757
- }
8758
- function parseSvgPath(d) {
8759
- const b = new PathBuilder();
8760
- const tokens = d.match(/[a-zA-Z]|-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g) ?? [];
8761
- let i = 0;
8762
- let cmd = "";
8763
- let x = 0, y = 0, sx = 0, sy = 0;
8764
- let lcx = 0, lcy = 0, lqx = 0, lqy = 0;
8765
- let prev = "";
8766
- const num = () => Number(tokens[i++]);
8767
- const more = () => i < tokens.length && !/[a-zA-Z]/.test(tokens[i]);
8768
- while (i < tokens.length) {
8769
- if (/[a-zA-Z]/.test(tokens[i])) cmd = tokens[i++];
8770
- const rel = cmd === cmd.toLowerCase();
8771
- const C = cmd.toUpperCase();
8772
- const ox = rel ? x : 0, oy = rel ? y : 0;
8773
- switch (C) {
8774
- case "M":
8775
- x = ox + num();
8776
- y = oy + num();
8777
- b.moveTo(x, y);
8778
- sx = x;
8779
- sy = y;
8780
- cmd = rel ? "l" : "L";
8781
- break;
8782
- case "L":
8783
- x = ox + num();
8784
- y = oy + num();
8785
- b.lineTo(x, y);
8786
- break;
8787
- case "H":
8788
- x = (rel ? x : 0) + num();
8789
- b.lineTo(x, y);
8790
- break;
8791
- case "V":
8792
- y = (rel ? y : 0) + num();
8793
- b.lineTo(x, y);
8794
- break;
8795
- case "C": {
8796
- const x1 = ox + num(), y1 = oy + num(), x2 = ox + num(), y2 = oy + num();
8797
- x = ox + num();
8798
- y = oy + num();
8799
- b.cubicTo(x1, y1, x2, y2, x, y);
8800
- lcx = x2;
8801
- lcy = y2;
8802
- break;
8803
- }
8804
- case "S": {
8805
- const r = prev === "C" || prev === "S";
8806
- const x1 = r ? 2 * x - lcx : x, y1 = r ? 2 * y - lcy : y;
8807
- const x2 = ox + num(), y2 = oy + num();
8808
- x = ox + num();
8809
- y = oy + num();
8810
- b.cubicTo(x1, y1, x2, y2, x, y);
8811
- lcx = x2;
8812
- lcy = y2;
8813
- break;
8814
- }
8815
- case "Q":
8816
- lqx = ox + num();
8817
- lqy = oy + num();
8818
- x = ox + num();
8819
- y = oy + num();
8820
- b.quadTo(lqx, lqy, x, y);
8821
- break;
8822
- case "T": {
8823
- const r = prev === "Q" || prev === "T";
8824
- lqx = r ? 2 * x - lqx : x;
8825
- lqy = r ? 2 * y - lqy : y;
8826
- x = ox + num();
8827
- y = oy + num();
8828
- b.quadTo(lqx, lqy, x, y);
8829
- break;
8830
- }
8831
- case "A": {
8832
- const rx = num(), ry = num(), phi = num() * Math.PI / 180, large = num() !== 0, sweep = num() !== 0;
8833
- const nx = ox + num(), ny = oy + num();
8834
- arcTo(b, x, y, rx, ry, phi, large, sweep, nx, ny);
8835
- x = nx;
8836
- y = ny;
8837
- break;
8838
- }
8839
- case "Z":
8840
- b.close();
8841
- x = sx;
8842
- y = sy;
8843
- break;
8844
- default: i++;
8845
- }
8846
- prev = C;
8847
- if (C === "Z" && more()) cmd = "L";
8848
- }
8849
- return b.build();
8850
- }
8851
- //#endregion
8852
8976
  //#region web/src/adapter/shapes.ts
8853
8977
  function flipPositionY(state, width, height) {
8854
8978
  const withPivotOffset = resolvePivotPosition(state, width, height);
@@ -8981,24 +9105,37 @@ function parse(svg, full) {
8981
9105
  function viaGeometry(Shape, state) {
8982
9106
  const shape = new Shape(state);
8983
9107
  const full = shape.fullState, geo = shape.geometry;
8984
- const base = parse(shape.buildSVGPath(geo), full);
8985
- const trims = shape.needsTrim();
8986
- const range = trims ? shape.getTrimRange() : null;
8987
- const fill = base && range ? trimPath(base, range.start, range.end) : base;
8988
- let stroke = fill;
8989
- const ring = shape instanceof EllipseGeometry ? shape.ringOutline(geo) : null;
8990
- if (ring != null) {
9108
+ const range = shape.needsTrim() ? shape.getTrimRange() : null;
9109
+ const base = once(() => {
9110
+ const outline = shape.buildPathData(geo);
9111
+ return outline && shapeTransform(full, outline);
9112
+ });
9113
+ const fill = once(() => {
9114
+ const p = base();
9115
+ return p && range ? trimPath(p, range.start, range.end) : p;
9116
+ });
9117
+ const stroke = once(() => {
9118
+ const ring = shape instanceof EllipseGeometry ? shape.ringOutline(geo) : null;
9119
+ if (ring == null) return fill();
8991
9120
  const outline = parse(ring, full);
8992
- stroke = outline && range ? trimPath(outline, range.start, range.end) : outline;
8993
- }
8994
- const interiorSvg = shape.buildSVGAlignInterior(geo);
8995
- const interior = interiorSvg != null ? parse(interiorSvg, full) : trims ? base : fill;
9121
+ return outline && range ? trimPath(outline, range.start, range.end) : outline;
9122
+ });
9123
+ const interior = once(() => {
9124
+ const svg = shape.buildSVGAlignInterior(geo);
9125
+ return svg != null ? parse(svg, full) : range ? base() : fill();
9126
+ });
8996
9127
  const b = shape.computeBounds(geo);
8997
9128
  const spread = shape.supportsSpread() ? (s) => parse(shape.buildSpreadSVGPath(geo, s), full) : void 0;
8998
9129
  return {
8999
- fill,
9000
- stroke,
9001
- interior,
9130
+ get fill() {
9131
+ return fill();
9132
+ },
9133
+ get stroke() {
9134
+ return stroke();
9135
+ },
9136
+ get interior() {
9137
+ return interior();
9138
+ },
9002
9139
  bounds: b ? [
9003
9140
  b.left,
9004
9141
  b.top,
@@ -9006,9 +9143,67 @@ function viaGeometry(Shape, state) {
9006
9143
  b.bottom
9007
9144
  ] : null,
9008
9145
  spread,
9009
- trimmed: !!range
9146
+ trimmed: !!range,
9147
+ native: range ? void 0 : nativeOf(shape, full, geo)
9148
+ };
9149
+ }
9150
+ function once(make) {
9151
+ let made = false;
9152
+ let value;
9153
+ return () => {
9154
+ if (!made) {
9155
+ value = make();
9156
+ made = true;
9157
+ }
9158
+ return value;
9010
9159
  };
9011
9160
  }
9161
+ /**
9162
+ * The shape the renderer draws itself: an unturned, unscaled rect whose corners
9163
+ * are all rounded (or square) and within half its shorter side, as the renderer
9164
+ * clamps them, or a whole ellipse with no hole.
9165
+ */
9166
+ function nativeOf(shape, full, geo) {
9167
+ if ((full.rotation ?? 0) !== 0 || (full.scale ?? 1) !== 1) return void 0;
9168
+ if (shape instanceof EllipseGeometry) {
9169
+ if (!geo.isFullShape || geo.ratio > 0) return void 0;
9170
+ const w = geo.halfWidth * 2, h = geo.halfHeight * 2;
9171
+ if (!(w > 0 && h > 0) || !Number.isFinite(w + h + geo.cx + geo.cy)) return void 0;
9172
+ return {
9173
+ kind: "ellipse",
9174
+ x: geo.cx - geo.halfWidth,
9175
+ y: geo.cy - geo.halfHeight,
9176
+ w,
9177
+ h,
9178
+ radii: [
9179
+ 0,
9180
+ 0,
9181
+ 0,
9182
+ 0
9183
+ ]
9184
+ };
9185
+ }
9186
+ if (shape instanceof RectGeometry) {
9187
+ const w = geo.right - geo.left, h = geo.bottom - geo.top;
9188
+ if (!(w > 0 && h > 0) || !Number.isFinite(w + h + geo.left + geo.top)) return void 0;
9189
+ const corners = [
9190
+ geo.tl,
9191
+ geo.tr,
9192
+ geo.br,
9193
+ geo.bl
9194
+ ];
9195
+ const limit = Math.min(w, h) / 2;
9196
+ if (corners.some((c) => !(c.radius >= 0 && c.radius <= limit) || c.radius > 0 && c.style === "angled")) return void 0;
9197
+ return {
9198
+ kind: "rect",
9199
+ x: geo.left,
9200
+ y: geo.top,
9201
+ w,
9202
+ h,
9203
+ radii: corners.map((c) => c.radius)
9204
+ };
9205
+ }
9206
+ }
9012
9207
  /** A path: data centred on its control-point bounds (or centerBounds), turned about the origin. */
9013
9208
  function pathShape(state) {
9014
9209
  const shape = new PathGeometry(state);
@@ -9411,6 +9606,29 @@ var SHAPE_OPS = new Set([
9411
9606
  "text",
9412
9607
  "richText"
9413
9608
  ]);
9609
+ var isSolid = (f) => f.type === "solid";
9610
+ /** `box` grown by `d` (shrunk when negative), each rounded corner with it; null where that is no longer the offset outline. */
9611
+ function offsetBox(box, d) {
9612
+ if (d === 0) return box;
9613
+ const w = box.w + 2 * d, h = box.h + 2 * d;
9614
+ if (!(w > 0 && h > 0)) return null;
9615
+ if (box.kind === "ellipse") return {
9616
+ ...box,
9617
+ x: box.x - d,
9618
+ y: box.y - d,
9619
+ w,
9620
+ h
9621
+ };
9622
+ if (box.radii.some((r) => r > 0 && !(r + d > 0))) return null;
9623
+ return {
9624
+ ...box,
9625
+ x: box.x - d,
9626
+ y: box.y - d,
9627
+ w,
9628
+ h,
9629
+ radii: box.radii.map((r) => r > 0 ? r + d : 0)
9630
+ };
9631
+ }
9414
9632
  var BLACK = {
9415
9633
  kind: "solid",
9416
9634
  color: [
@@ -9995,6 +10213,7 @@ var WgpuRenderContext = class extends CanvasRenderContext2D {
9995
10213
  paintFill(layers) {
9996
10214
  this.paintText(layers, null);
9997
10215
  if (!layers.length) return;
10216
+ if (this.fillNative(layers)) return;
9998
10217
  const path = this.surface("fill");
9999
10218
  const geoms = path ? [{ path }] : [];
10000
10219
  for (const s of this.shapes) if (s.members) geoms.push({ members: s.members });
@@ -10004,6 +10223,84 @@ var WgpuRenderContext = class extends CanvasRenderContext2D {
10004
10223
  this.shadows = [];
10005
10224
  for (const g of geoms) this.fillGeom(g, bounds, layers, all);
10006
10225
  }
10226
+ /**
10227
+ * One rect or ellipse painted solid, with no shadow to cast, drawn as the
10228
+ * renderer's own shape. Gradients and images stay paths: their paint space
10229
+ * is the path's.
10230
+ */
10231
+ fillNative(layers) {
10232
+ const shape = this.shapes.length === 1 ? this.shapes[0] : null;
10233
+ const box = shape?.native;
10234
+ if (!shape || !box || this.inVectorMask || this.shadows.length || !layers.every(isSolid)) return false;
10235
+ for (const f of layers) {
10236
+ const paint = fillPaint(f, shape.bounds, this.missing, this.assets);
10237
+ if (paint) this.native(this.cur, box, {
10238
+ fill: paint,
10239
+ opacity: this.worldAlpha * (f.opacity ?? 1),
10240
+ blend: this.blendOf(f)
10241
+ });
10242
+ }
10243
+ return true;
10244
+ }
10245
+ /**
10246
+ * An undashed solid stroke round one rect, or round a circle (an ellipse's
10247
+ * offset outline is no ellipse), drawn as the renderer's own shape. Biased by
10248
+ * `align`, the band is a centred stroke round the shape offset by
10249
+ * align·weight/2, which stays exact while no rounded corner shrinks away. The
10250
+ * renderer keeps a square corner sharp, which is a miter join.
10251
+ */
10252
+ strokeNative(st, weight, intWidth, dev) {
10253
+ const shape = this.shapes.length === 1 ? this.shapes[0] : null;
10254
+ const native = shape?.native;
10255
+ if (!shape || !native || weight <= 0 || (st.dash?.length ?? 0) > 0 || !st.fill.every(isSolid)) return false;
10256
+ if (native.kind === "ellipse" && Math.abs(native.w - native.h) > .001) return false;
10257
+ if (native.kind === "rect" && native.radii.some((r) => r === 0) && (st.join !== "miter" || !(st.miterLimit >= Math.SQRT2))) return false;
10258
+ const a = Math.max(-1, Math.min(1, st.align ?? 0));
10259
+ const box = offsetBox(native, a * weight / 2);
10260
+ if (!box) return false;
10261
+ const b = shape.bounds;
10262
+ const snapM = this.snapBounds([
10263
+ Math.fround(b[0]),
10264
+ Math.fround(b[1]),
10265
+ Math.fround(b[2]),
10266
+ Math.fround(b[3])
10267
+ ], intWidth, dev);
10268
+ const parent = snapM ? this.doc.group(this.cur, { transform: snapM }) : this.cur;
10269
+ for (const f of st.fill) {
10270
+ const paint = fillPaint(f, b, this.missing, this.assets);
10271
+ if (!paint) continue;
10272
+ const stroke = {
10273
+ paint,
10274
+ width: weight,
10275
+ join: st.join,
10276
+ cap: st.cap,
10277
+ miter: st.miterLimit
10278
+ };
10279
+ this.native(parent, box, {
10280
+ opacity: this.worldAlpha * (f.opacity ?? 1),
10281
+ stroke,
10282
+ ...a === 0 && { blend: this.blendOf(f) }
10283
+ });
10284
+ }
10285
+ return true;
10286
+ }
10287
+ native(parent, box, props) {
10288
+ const at = {
10289
+ ...props,
10290
+ transform: [
10291
+ 1,
10292
+ 0,
10293
+ 0,
10294
+ 1,
10295
+ box.x,
10296
+ box.y
10297
+ ],
10298
+ w: box.w,
10299
+ h: box.h,
10300
+ radii: box.radii
10301
+ };
10302
+ return box.kind === "rect" ? this.doc.rect(parent, at) : this.doc.ellipse(parent, at);
10303
+ }
10007
10304
  fillGeom(geom, bounds, layers, all) {
10008
10305
  if (this.inVectorMask) {
10009
10306
  this.leaf(this.cur, geom, { fill: BLACK });
@@ -10494,8 +10791,10 @@ var WgpuRenderContext = class extends CanvasRenderContext2D {
10494
10791
  /** StrokeHandler.snapPath: an axis-aligned thin stroke's shape scaled and
10495
10792
  * moved so its device bounds sit on pixel edges (centres for odd widths). */
10496
10793
  snap(path, intWidth, m) {
10794
+ return this.snapBounds(pathBounds(path), intWidth, m);
10795
+ }
10796
+ snapBounds([bx0, by0, bx1, by1], intWidth, m) {
10497
10797
  if (intWidth <= 0 || Math.abs(m[1]) > 1e-6 || Math.abs(m[2]) > 1e-6 || m[0] === 0 || m[3] === 0) return null;
10498
- const [bx0, by0, bx1, by1] = pathBounds(path);
10499
10798
  if (bx1 - bx0 <= 0 || by1 - by0 <= 0) return null;
10500
10799
  const sx = m[0], sy = m[3], tx = m[4], ty = m[5];
10501
10800
  const l = tx + sx * bx0, t = ty + sy * by0, r = tx + sx * bx1, b = ty + sy * by1;
@@ -10521,7 +10820,8 @@ var WgpuRenderContext = class extends CanvasRenderContext2D {
10521
10820
  }
10522
10821
  paintStroke(strokes) {
10523
10822
  for (const st of strokes) this.paintText(st.fill, st);
10524
- const fillAll = this.surface("fill");
10823
+ let fillAll;
10824
+ const surfaceFill = () => fillAll === void 0 ? fillAll = this.surface("fill") : fillAll;
10525
10825
  const dev = this.device();
10526
10826
  const scale = Math.sqrt(Math.abs(dev[0] * dev[3] - dev[1] * dev[2]));
10527
10827
  const rotated = isProjective(dev) || Math.abs(dev[1]) > 1e-6 || Math.abs(dev[2]) > 1e-6;
@@ -10531,6 +10831,7 @@ var WgpuRenderContext = class extends CanvasRenderContext2D {
10531
10831
  intWidth = Math.max(1, Math.round(weight * scale));
10532
10832
  weight = intWidth / scale;
10533
10833
  }
10834
+ if (this.strokeNative(st, weight, intWidth, dev)) continue;
10534
10835
  for (const shape of this.strokeShapes()) {
10535
10836
  let geom, interior;
10536
10837
  let plan = {
@@ -10548,7 +10849,7 @@ var WgpuRenderContext = class extends CanvasRenderContext2D {
10548
10849
  geom = { path: p.source ?? shape.stroke };
10549
10850
  interior = shape.alignInterior ? { members: shape.alignInterior } : shape.interior && isClosed(shape.interior) ? { path: shape.interior } : null;
10550
10851
  }
10551
- const bounds = this.bounds(fillAll ?? ("path" in geom ? geom.path : null));
10852
+ const bounds = this.bounds(surfaceFill() ?? ("path" in geom ? geom.path : null));
10552
10853
  const a = Math.max(-1, Math.min(1, st.align ?? 0));
10553
10854
  const halves = a !== 0 && interior !== null && !plan.ends.some((e) => isStrokeMarker(e.cap)) ? [[(1 + a) * weight, "out"], [(1 - a) * weight, "in"]] : [[weight, "both"]];
10554
10855
  const snapM = shape.fill ? this.snap(shape.fill, intWidth, dev) : null;
@@ -10587,7 +10888,7 @@ var WgpuRenderContext = class extends CanvasRenderContext2D {
10587
10888
  * whole, else their contours together (a union would close open ones),
10588
10889
  * aligned against the union. Text keeps each shape apart. */
10589
10890
  strokeShapes() {
10590
- if (this.shapes.some((s) => s.text)) return this.shapes;
10891
+ if (this.shapes.length < 2 || this.shapes.some((s) => s.text)) return this.shapes;
10591
10892
  const withPaths = this.shapes.filter((s) => s.members || s.fill && s.fill.verbs.length).sort((a, b) => (b.members ? 1 : 0) - (a.members ? 1 : 0));
10592
10893
  if (withPaths.length < 2) return this.shapes;
10593
10894
  const members = [];