@weasel-js/text 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { S as StyledRun, T as TextStyle, L as LaidOutRuns, R as ResolvedTextStyle, a as ResolvedRun, b as LayoutRunsOpts, c as TextPaint } from './test-seams-BypGSlZj.js';
2
- export { B as BidiAnalysis, d as BidiReordering, e as BidiResolver, D as DEFAULT_TEXT_STYLE, F as FontVariantCaps, f as LAYOUT_CACHE_STRUCTURAL_LIMIT, g as LAYOUT_CACHE_VARIANT_LIMIT, h as LaidOutCell, i as LaidOutLineBox, M as MARKDOWN_RUN_GRAMMAR, P as PlacedGlyph, j as RunFlag, k as RunGrammar, l as RunMarker, m as RunSourceMap, n as SCRIPT_METRICS, o as SMALL_CAPS_SCALE, p as ScriptPreset, q as SmallCapsText, r as TextAlign, s as TextDirection, t as TextTransform, u as TransformedRunText, v as cachedLayoutRuns, w as fontString, x as isBoldWeight, y as isSmallCapsLetter, z as layoutRuns, A as markdownToRuns, C as numericWeight, E as paragraphDirection, G as registerBidi, H as resolveAlign, I as resolveRunFace, J as resolveRuns, K as resolveTextStyle, N as runsToMarkdown, O as runsToPlainText, Q as scriptMetrics, U as scriptMetricsFor, V as smallCapsScale, W as smallCapsScaleFor, X as smallCapsText, Y as toRuns, Z as transformRunTexts } from './test-seams-BypGSlZj.js';
3
- import { FaceRuleMetrics, FaceMetrics, FontStyle, BmFontChar } from '@weasel-js/font';
1
+ import { S as StyledRun, T as TextStyle, L as LaidOutRuns, R as ResolvedTextStyle, a as ResolvedRun, b as LayoutRunsOpts, c as TextPaint } from './test-seams-B0BDm36M.js';
2
+ export { B as BidiAnalysis, d as BidiReordering, e as BidiResolver, D as DEFAULT_TEXT_FILL, f as DEFAULT_TEXT_STYLE, F as FontVariantCaps, g as LAYOUT_CACHE_STRUCTURAL_LIMIT, h as LAYOUT_CACHE_VARIANT_LIMIT, i as LaidOutCell, j as LaidOutLineBox, M as MARKDOWN_RUN_GRAMMAR, P as PlacedGlyph, k as RunFlag, l as RunGrammar, m as RunMarker, n as RunSourceMap, o as SCRIPT_METRICS, p as SMALL_CAPS_SCALE, q as ScriptPreset, r as SmallCapsText, s as TextAlign, t as TextDirection, u as TextTransform, v as TransformedRunText, w as cachedLayoutRuns, x as fontString, y as isBoldWeight, z as isSmallCapsLetter, A as layoutRuns, C as markdownToRuns, E as numericWeight, G as paragraphDirection, H as registerBidi, I as resolveAlign, J as resolveRunFace, K as resolveRuns, N as resolveTextStyle, O as runsToMarkdown, Q as runsToPlainText, U as scriptMetrics, V as scriptMetricsFor, W as smallCapsScale, X as smallCapsScaleFor, Y as smallCapsText, Z as toRuns, _ as transformRunTexts } from './test-seams-B0BDm36M.js';
3
+ import { FaceRuleMetrics, FaceMetrics, FontStyle, BmFontChar, ResolveResult } from '@weasel-js/font';
4
4
  import { Fill, Stroke } from '@weasel-js/paint';
5
- import { Rect, PathInWorldPose, PolygonPath } from '@weasel-js/geom';
5
+ import { Rect, PolygonPath, PathInWorldPose } from '@weasel-js/geom';
6
6
  import { Mixed } from '@weasel-js/registry';
7
7
  export { MIXED, Mixed } from '@weasel-js/registry';
8
8
 
@@ -162,9 +162,10 @@ interface LaidOutQuad {
162
162
  * One glyph the renderer should tessellate rather than sample.
163
163
  *
164
164
  * `d` is em-space SVG path data — 1 unit is 1 em, y grows down, origin on the
165
- * baseline at the pen. Placing it is therefore a uniform scale by `scale`
165
+ * baseline at the pen — with any synthetic bold already grown into it. Placing it is therefore a uniform scale by `scale`
166
166
  * followed by a translate to `(x, baselineY)`. `key` identifies the cached
167
- * tessellation — face identity plus codepoint.
167
+ * tessellation — face identity plus codepoint, plus the growth of a synthetic
168
+ * bold.
168
169
  */
169
170
  interface LaidOutOutlineGlyph {
170
171
  d: string;
@@ -189,7 +190,8 @@ interface LaidOutGroup {
189
190
  /** Resolved variant — matches the registered atlas and the texture-cache key. */
190
191
  weight: number;
191
192
  style: FontStyle;
192
- /** Gap between the request and the resolved match, for the renderer to fake. */
193
+ /** Gap between the request and the resolved match, for the renderer to fake.
194
+ * An `'outline'` group's bold is already grown into its glyphs. */
193
195
  synthetic: {
194
196
  bold: boolean;
195
197
  italic: boolean;
@@ -209,7 +211,7 @@ interface LaidOutGroup {
209
211
  /** Outline painted over the glyphs. Only ever set on an `'outline'` group —
210
212
  * the SDF tiers have no geometry to stroke, which is why a stroke pulls its
211
213
  * run onto the outline tier at any size. A run the tier cannot serve at all
212
- * (no registered outlines, synthetic bold) stays on the atlas, unstroked. */
214
+ * (no registered outlines) stays on the atlas, unstroked. */
213
215
  stroke?: Stroke;
214
216
  /** Textured glyph quads. Always empty for an `'outline'` group. */
215
217
  quads: LaidOutQuad[];
@@ -659,6 +661,62 @@ declare function setFlagOverRange(runs: readonly StyledRun[], style: TextStyle,
659
661
  */
660
662
  declare const SYNTHETIC_ITALIC_RADIANS = 0.2094;
661
663
 
664
+ /**
665
+ * Synthetic bold as geometry. The renderer fakes a missing bold face by
666
+ * dropping the distance-field threshold, which moves every edge of the glyph
667
+ * outward by `syntheticBoldGrowth`; outlining it is the glyph's own outline
668
+ * grown by that distance.
669
+ */
670
+
671
+ /**
672
+ * How far synthetic bold grows a glyph served as `resolved`, in em per side,
673
+ * and how its corners turn; `null` when the renderer draws it unshifted.
674
+ * An MSDF atlas keeps sharp corners, so its shifted edge meets in a miter; a
675
+ * canvas bake is a true distance field, whose shifted edge rounds them.
676
+ */
677
+ declare function syntheticBoldOffset(resolved: ResolveResult): {
678
+ growth: number;
679
+ join: 'miter' | 'round';
680
+ } | null;
681
+ /**
682
+ * {@link emboldenGlyph} for the renderer's outline tier, which takes outline
683
+ * data: `d` grown as the field draws it, and a tag naming the growth for the
684
+ * glyph's tessellation key. `null` when the renderer draws `resolved` unshifted.
685
+ */
686
+ declare function emboldenGlyphD(d: string, resolved: ResolveResult): {
687
+ d: string;
688
+ tag: string;
689
+ } | null;
690
+
691
+ /**
692
+ * The per-glyph half of text-as-geometry, shared by `textToPath` and
693
+ * `textClusters`: checking a run's face can give outlines, finding a placed
694
+ * glyph's outline, and setting it where the renderer's outline tier puts it.
695
+ */
696
+
697
+ /**
698
+ * Why a text node has no outline geometry to give:
699
+ *
700
+ * - `'no-outlines'` — the face that serves it has no outlines registered
701
+ * (`registerFontOutlines`), so there is no geometry to read.
702
+ * - `'outlines-loading'` — registered but not parsed yet. `loadTextOutlines`
703
+ * waits for it; the call has already started the load.
704
+ * - `'outlines-failed'` — registered, and the bytes did not load or parse.
705
+ * - `'color-glyph'` — the text holds an emoji, which is a picture in its own
706
+ * colors with no outline. Nothing a caller registers changes that.
707
+ */
708
+ type TextOutlinesFailure = 'no-outlines' | 'color-glyph' | 'outlines-loading' | 'outlines-failed';
709
+ /** Thrown by `textToPath`, `textToPathsByPaint` and `textClusters` when some
710
+ * run of the text has no outline geometry. Names the run's requested face and
711
+ * the function that was asked. */
712
+ declare class TextOutlinesError extends Error {
713
+ readonly reason: TextOutlinesFailure;
714
+ readonly family: string;
715
+ readonly weight: number;
716
+ readonly style: FontStyle;
717
+ constructor(reason: TextOutlinesFailure, family: string, weight: number, style: FontStyle, detail: string, caller?: string);
718
+ }
719
+
662
720
  /**
663
721
  * Text as path geometry: the glyph outlines a text node draws, placed in
664
722
  * world space and composed into one `Path`. The glyphs are the ones
@@ -699,27 +757,6 @@ interface TextToPathOptions {
699
757
  * against, as for `textCommandFromPose`. Default 1. */
700
758
  scale?: number;
701
759
  }
702
- /**
703
- * Why a text node has no outline geometry to give:
704
- *
705
- * - `'no-outlines'` — the face that serves it has no outlines registered
706
- * (`registerFontOutlines`), so there is no geometry to read.
707
- * - `'outlines-loading'` — registered but not parsed yet. `loadTextOutlines`
708
- * waits for it; the call has already started the load.
709
- * - `'outlines-failed'` — registered, and the bytes did not load or parse.
710
- * - `'color-glyph'` — the text holds an emoji, which is a picture in its own
711
- * colors with no outline. Nothing a caller registers changes that.
712
- */
713
- type TextOutlinesFailure = 'no-outlines' | 'color-glyph' | 'outlines-loading' | 'outlines-failed';
714
- /** Thrown by {@link textToPath} when some run of the text has no outline
715
- * geometry. Names the run's requested face. */
716
- declare class TextOutlinesError extends Error {
717
- readonly reason: TextOutlinesFailure;
718
- readonly family: string;
719
- readonly weight: number;
720
- readonly style: FontStyle;
721
- constructor(reason: TextOutlinesFailure, family: string, weight: number, style: FontStyle, detail: string);
722
- }
723
760
  /**
724
761
  * The outline geometry of a text node, in world space: every glyph and every
725
762
  * underline, strikethrough and overline rule it draws, as one `'nonzero'`
@@ -752,4 +789,37 @@ declare function textToPathsByPaint(data: TextOutlineSource, pose: PathInWorldPo
752
789
  */
753
790
  declare function loadTextOutlines(data: TextOutlineSource, opts?: TextToPathOptions): Promise<void>;
754
791
 
755
- export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_DECORATION_METRICS, type DecorationKind, type FaceMetricsFn, type FitTextPoseOptions, type FlagKey, type LabelOptions, type LaidOutDecoration, type LaidOutGroup, type LaidOutOutlineGlyph, type LaidOutQuad, LaidOutRuns, type LayoutLine, type LayoutResult, LayoutRunsOpts, type MarkdownFontOptions, type MeasureFn, type MeasureTextBoundsOpts, type MeasuredSpan, type MeasuredText, NO_BREAK, type PaintRunsOpts, type PaintedRuns, type PointInTextPoseOpts, type PositionedRun, type RangeStyle, ResolvedRun, ResolvedTextStyle, type RunStylePatch, SYNTHETIC_ITALIC_RADIANS, type SetFlagResult, type StyleKey, StyledRun, type TextLineBoxesOpts, type TextNodeSource, type TextOutlineSource, TextOutlinesError, type TextOutlinesFailure, TextPaint, type TextPaintPath, type TextPose, type TextPoseLayout, type TextPoseLayoutInput, type TextRenderer, TextStyle, type TextToPathOptions, type TextVerticalAlign, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, fitTextPose, isHardLineBreak, isTextData, layoutMarkdown, layoutTextPose, lineBreakOpportunities, loadTextOutlines, measureText, measureTextBounds, measuredWidth, nodeHasFlag, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, runsCarryStyling, setFlagOverRange, styleAtRange, supersededKey, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, unboldPatch, verticalAlignOffset };
792
+ /** One grapheme cluster of a text line: its slot and its outlines. */
793
+ interface TextCluster {
794
+ /** Its slot's leading edge, from the line's left, in visual order. */
795
+ x: number;
796
+ /** The slot's width: its cells' advances, kerning and tracking included. */
797
+ advance: number;
798
+ /** Outlines with the slot's left edge at x = 0 and the baseline at y = 0, y down, one per paint; empty for a
799
+ * space or a color glyph. */
800
+ paths: TextPaintPath[];
801
+ /** Height of the x-height's middle above the baseline. */
802
+ middle: number;
803
+ }
804
+ /** Options for {@link textClusters}. */
805
+ interface TextClustersOptions extends TextToPathOptions {
806
+ /** The node's box width, which a `wrap` style wraps at and `align` aligns
807
+ * in, as the node's own layout does. Default `Infinity`: one line per
808
+ * paragraph. */
809
+ width?: number;
810
+ }
811
+ /**
812
+ * The first laid line of `data` as its grapheme clusters, in visual order,
813
+ * laid at the node's box origin with no pose applied; the lines after it are
814
+ * dropped. The slots tile the
815
+ * line: each cluster's `advance` runs to the next one's `x`, so the kerning
816
+ * between two clusters belongs to the left one. Decoration rules are left out.
817
+ *
818
+ * Throws `TextOutlinesError` as `textToPath` does, except that a color
819
+ * glyph, or one drawn by a fallback face with no outlines, keeps its slot
820
+ * with no `paths`. Only a code point that inks its slot is drawn, so a color
821
+ * cluster's leading code points stay blank too.
822
+ */
823
+ declare function textClusters(data: TextOutlineSource, opts?: TextClustersOptions): TextCluster[];
824
+
825
+ export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_DECORATION_METRICS, type DecorationKind, type FaceMetricsFn, type FitTextPoseOptions, type FlagKey, type LabelOptions, type LaidOutDecoration, type LaidOutGroup, type LaidOutOutlineGlyph, type LaidOutQuad, LaidOutRuns, type LayoutLine, type LayoutResult, LayoutRunsOpts, type MarkdownFontOptions, type MeasureFn, type MeasureTextBoundsOpts, type MeasuredSpan, type MeasuredText, NO_BREAK, type PaintRunsOpts, type PaintedRuns, type PointInTextPoseOpts, type PositionedRun, type RangeStyle, ResolvedRun, ResolvedTextStyle, type RunStylePatch, SYNTHETIC_ITALIC_RADIANS, type SetFlagResult, type StyleKey, StyledRun, type TextCluster, type TextClustersOptions, type TextLineBoxesOpts, type TextNodeSource, type TextOutlineSource, TextOutlinesError, type TextOutlinesFailure, TextPaint, type TextPaintPath, type TextPose, type TextPoseLayout, type TextPoseLayoutInput, type TextRenderer, TextStyle, type TextToPathOptions, type TextVerticalAlign, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, emboldenGlyphD, fitTextPose, isHardLineBreak, isTextData, layoutMarkdown, layoutTextPose, lineBreakOpportunities, loadTextOutlines, measureText, measureTextBounds, measuredWidth, nodeHasFlag, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, runsCarryStyling, setFlagOverRange, styleAtRange, supersededKey, syntheticBoldOffset, textClusters, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, unboldPatch, verticalAlignOffset };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, tabAdvance, widthWithTabs, layoutRuns } from './chunk-HI4VEOYP.js';
2
- export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, cachedLayoutRuns, fontString, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle } from './chunk-HI4VEOYP.js';
1
+ import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, layoutRuns, tabAdvance, widthWithTabs } from './chunk-ADRQIIHS.js';
2
+ export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_TEXT_FILL, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, cachedLayoutRuns, fontString, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle } from './chunk-ADRQIIHS.js';
3
3
  import { resolveScreenLength, fillHasInk, strokeHasInk, paintKey, strokeKey } from '@weasel-js/paint';
4
- import { faceMetricsOf, resolveFontVariant, glyphGeneration, loadFontOutlines, faceMetricsFor, glyphOutline, outlineStatus, cssFamilyName, syntheticBoldGrowth } from '@weasel-js/font';
4
+ import { faceMetricsOf, resolveFontVariant, syntheticBoldGrowth, glyphGeneration, loadFontOutlines, faceMetricsFor, glyphOutline, outlineStatus, cssFamilyName } from '@weasel-js/font';
5
+ import { pathToD, pathFromD, mergeRegion, growRegion, poseFrameOf, transformPath, PATH_M, PATH_L, PATH_Z, pathSignedArea, reversePath } from '@weasel-js/geom';
5
6
  import { MIXED, deepEqual } from '@weasel-js/registry';
6
7
  export { MIXED } from '@weasel-js/registry';
7
- import { transformPath, poseFrameOf, growRegion, pathFromD, pathSignedArea, reversePath, PATH_M, PATH_L, PATH_Z } from '@weasel-js/geom';
8
8
 
9
9
  // src/runs.ts
10
10
  function toRuns(input) {
@@ -413,8 +413,8 @@ function lineDecorations(glyphs, runs, faceOf, paintOf) {
413
413
  };
414
414
  for (const g of glyphs) {
415
415
  const style = runs[g.run];
416
- const paint = paintOf(g.run);
417
- if (paint === null || !DECORATION_KINDS.some((kind) => style[kind])) {
416
+ const paint = style.underline || style.strikethrough || style.overline ? paintOf(g.run) : null;
417
+ if (paint === null) {
418
418
  close();
419
419
  continue;
420
420
  }
@@ -465,13 +465,61 @@ function layoutTextPose(pose, scale = 1) {
465
465
  y: pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height)
466
466
  };
467
467
  }
468
+ var SYNTHETIC_BOLD_TOLERANCE = 5e-4;
469
+ var CACHE_LIMIT = 4096;
470
+ var cache = /* @__PURE__ */ new Map();
471
+ var dCache = /* @__PURE__ */ new Map();
472
+ function syntheticBoldOffset(resolved2) {
473
+ if (!resolved2.synthetic.bold) return null;
474
+ const { source } = resolved2;
475
+ if (source !== "atlas" && source !== "canvas") return null;
476
+ const { family, weight, style } = resolved2.resolved;
477
+ const growth = syntheticBoldGrowth(source, family, weight, style);
478
+ if (growth === void 0 || !(growth > 0)) return null;
479
+ return { growth, join: source === "atlas" ? "miter" : "round" };
480
+ }
481
+ function emboldenGlyph(em, d, resolved2) {
482
+ const offset = syntheticBoldOffset(resolved2);
483
+ if (offset === null) return em;
484
+ const key = `${offset.growth}|${offset.join}|${d}`;
485
+ let grown = cache.get(key);
486
+ if (grown) return grown;
487
+ grown = mergeRegion(
488
+ growRegion(em, offset.growth, { join: offset.join, tolerance: SYNTHETIC_BOLD_TOLERANCE }),
489
+ { tolerance: SYNTHETIC_BOLD_TOLERANCE }
490
+ );
491
+ if (cache.size >= CACHE_LIMIT) cache.clear();
492
+ cache.set(key, grown);
493
+ return grown;
494
+ }
495
+ function emboldenGlyphD(d, resolved2) {
496
+ const offset = syntheticBoldOffset(resolved2);
497
+ if (offset === null) return null;
498
+ const tag = `bold:${offset.growth}:${offset.join}`;
499
+ const key = `${tag}|${d}`;
500
+ let grown = dCache.get(key);
501
+ if (grown === void 0) {
502
+ grown = pathToD(emboldenGlyph(pathFromD(d), d, resolved2));
503
+ if (dCache.size >= CACHE_LIMIT) dCache.clear();
504
+ dCache.set(key, grown);
505
+ }
506
+ return { d: grown, tag };
507
+ }
508
+
509
+ // src/layout/paintRuns.ts
468
510
  function glyphOutlineD(g, run, minSize) {
469
- if (g.resolved.source !== "outline") {
511
+ const outlineOnly = g.resolved.source === "outline";
512
+ if (!outlineOnly) {
470
513
  if (minSize === void 0) return null;
471
514
  if (run.fontSize < minSize && !strokeHasInk(run.stroke)) return null;
472
- if (g.resolved.synthetic.bold) return null;
473
515
  }
474
- return faceOutlineD(g);
516
+ const d = faceOutlineD(g);
517
+ if (d === null) return null;
518
+ const { family, weight, style } = g.resolved.resolved;
519
+ const key = `${family}|${weight}|${style}|${g.cp}`;
520
+ if (outlineOnly || !g.resolved.synthetic.bold) return { d, key };
521
+ const bold = emboldenGlyphD(d, g.resolved);
522
+ return bold && { d: bold.d, key: `${key}|${bold.tag}` };
475
523
  }
476
524
  function faceOutlineD(g) {
477
525
  if (g.glyph?.color) return null;
@@ -501,10 +549,13 @@ function runFaces(runs) {
501
549
  const faces = runs.map((r) => faceMetricsOf(resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle)));
502
550
  return (run) => faces[run];
503
551
  }
504
- function groupFor(groups, g, run, source) {
552
+ function groupFor(groups, g, run, source, last) {
505
553
  const { family, weight, style } = g.resolved.resolved;
506
554
  const { synthetic } = g.resolved;
507
555
  const page = source === "outline" ? 0 : g.glyph.page;
556
+ if (last.group !== null && last.run === g.run && last.source === source && last.page === page && last.family === family && last.weight === weight && last.style === style && last.bold === synthetic.bold && last.italic === synthetic.italic) {
557
+ return last.group;
558
+ }
508
559
  const stroke = source === "outline" && strokeHasInk(run.stroke) ? run.stroke : void 0;
509
560
  const key = `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${paintKey(run.fill?.paint)}|${strokeKey(stroke)}|${source}|${page}`;
510
561
  let group = groups.get(key);
@@ -523,23 +574,41 @@ function groupFor(groups, g, run, source) {
523
574
  };
524
575
  groups.set(key, group);
525
576
  }
577
+ last.group = group;
578
+ last.run = g.run;
579
+ last.family = family;
580
+ last.weight = weight;
581
+ last.style = style;
582
+ last.bold = synthetic.bold;
583
+ last.italic = synthetic.italic;
584
+ last.source = source;
585
+ last.page = page;
526
586
  return group;
527
587
  }
528
588
  function paintRuns(laid, runs, opts = {}) {
529
589
  const groups = /* @__PURE__ */ new Map();
590
+ const last = {
591
+ group: null,
592
+ run: -1,
593
+ family: "",
594
+ weight: 0,
595
+ style: "normal",
596
+ bold: false,
597
+ italic: false,
598
+ source: "atlas",
599
+ page: 0
600
+ };
530
601
  const decorations = [];
531
602
  const faceOf = runFaces(runs);
532
603
  const rulePaint = (run) => fillHasInk(runs[run].fill) ? paintKey(runs[run].fill.paint) : null;
533
604
  for (const line of laid.lines) {
534
605
  for (const g of line.glyphs) {
535
606
  const run = runs[g.run];
536
- const d = glyphOutlineD(g, run, opts.outlineMinSize);
537
- if (!fillHasInk(run.fill) && !(d !== null && strokeHasInk(run.stroke))) continue;
538
- if (d !== null) {
539
- const { family, weight, style } = g.resolved.resolved;
540
- groupFor(groups, g, run, "outline").glyphs.push({
541
- d,
542
- key: `${family}|${weight}|${style}|${g.cp}`,
607
+ const outline = glyphOutlineD(g, run, opts.outlineMinSize);
608
+ if (!fillHasInk(run.fill) && !(outline !== null && strokeHasInk(run.stroke))) continue;
609
+ if (outline !== null) {
610
+ groupFor(groups, g, run, "outline", last).glyphs.push({
611
+ ...outline,
543
612
  x: g.x,
544
613
  baselineY: g.baselineY,
545
614
  scale: g.fontSize
@@ -548,8 +617,8 @@ function paintRuns(laid, runs, opts = {}) {
548
617
  }
549
618
  const quad = glyphQuad(g);
550
619
  if (!quad) continue;
551
- if (g.glyph?.color) groupFor(groups, g, run, "color").quads.push({ ...quad, colorGlyph: g.glyph });
552
- else groupFor(groups, g, run, g.resolved.source).quads.push(quad);
620
+ if (g.glyph?.color) groupFor(groups, g, run, "color", last).quads.push({ ...quad, colorGlyph: g.glyph });
621
+ else groupFor(groups, g, run, g.resolved.source, last).quads.push(quad);
553
622
  }
554
623
  for (const rule of lineDecorations(line.glyphs, runs, faceOf, rulePaint)) {
555
624
  const { run, ...rect } = rule;
@@ -1233,41 +1302,13 @@ function effectiveRangeStyle(range, style, paint) {
1233
1302
 
1234
1303
  // src/syntheticItalic.ts
1235
1304
  var SYNTHETIC_ITALIC_RADIANS = 0.2094;
1236
- var SYNTHETIC_BOLD_TOLERANCE = 5e-4;
1237
- var CACHE_LIMIT = 4096;
1238
- var cache = /* @__PURE__ */ new Map();
1239
- function syntheticBoldOffset(resolved2) {
1240
- if (!resolved2.synthetic.bold) return null;
1241
- const { source } = resolved2;
1242
- if (source !== "atlas" && source !== "canvas") return null;
1243
- const { family, weight, style } = resolved2.resolved;
1244
- const growth = syntheticBoldGrowth(source, family, weight, style);
1245
- if (growth === void 0 || !(growth > 0)) return null;
1246
- return { growth, join: source === "atlas" ? "miter" : "round" };
1247
- }
1248
- function emboldenGlyph(em, d, resolved2) {
1249
- const offset = syntheticBoldOffset(resolved2);
1250
- if (offset === null) return em;
1251
- const key = `${offset.growth}|${offset.join}|${d}`;
1252
- let grown = cache.get(key);
1253
- if (grown) return grown;
1254
- grown = growRegion(em, offset.growth, {
1255
- join: offset.join,
1256
- tolerance: SYNTHETIC_BOLD_TOLERANCE
1257
- });
1258
- if (cache.size >= CACHE_LIMIT) cache.clear();
1259
- cache.set(key, grown);
1260
- return grown;
1261
- }
1262
-
1263
- // src/textToPath.ts
1264
1305
  var TextOutlinesError = class extends Error {
1265
1306
  reason;
1266
1307
  family;
1267
1308
  weight;
1268
1309
  style;
1269
- constructor(reason, family, weight, style, detail) {
1270
- super(`textToPath: "${family}" ${weight}/${style} \u2014 ${detail}`);
1310
+ constructor(reason, family, weight, style, detail, caller = "textToPath") {
1311
+ super(`${caller}: "${family}" ${weight}/${style} \u2014 ${detail}`);
1271
1312
  this.name = "TextOutlinesError";
1272
1313
  this.reason = reason;
1273
1314
  this.family = family;
@@ -1275,22 +1316,10 @@ var TextOutlinesError = class extends Error {
1275
1316
  this.style = style;
1276
1317
  }
1277
1318
  };
1278
- function facesOf(runs) {
1279
- const out = /* @__PURE__ */ new Map();
1280
- const add = (family, weight, style) => {
1281
- out.set(`${family}|${weight}|${style}`, { family, weight, style });
1282
- };
1283
- for (const r of runs) {
1284
- add(r.fontFamily, r.fontWeight, r.fontStyle);
1285
- const { family, weight, style } = resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle).resolved;
1286
- add(family, weight, style);
1287
- }
1288
- return [...out.values()];
1289
- }
1290
- function assertOutlinable(run) {
1319
+ function assertOutlinable(run, caller) {
1291
1320
  const { fontFamily, fontWeight, fontStyle } = run;
1292
1321
  const fail = (reason, detail) => {
1293
- throw new TextOutlinesError(reason, fontFamily, fontWeight, fontStyle, detail);
1322
+ throw new TextOutlinesError(reason, fontFamily, fontWeight, fontStyle, detail, caller);
1294
1323
  };
1295
1324
  const requested = outlineStatus(fontFamily, fontWeight, fontStyle);
1296
1325
  if (requested === "idle" || requested === "loading") {
@@ -1312,6 +1341,21 @@ function assertOutlinable(run) {
1312
1341
  fail("outlines-failed", `the outlines registered for ${family} ${weight}/${style} failed to load.`);
1313
1342
  }
1314
1343
  }
1344
+ function paintsOf(runs) {
1345
+ const paints = [];
1346
+ const indexOf = /* @__PURE__ */ new Map();
1347
+ const paintOf = runs.map((run) => {
1348
+ const key = `${paintKey(run.fill?.paint)}|${strokeKey(run.stroke)}`;
1349
+ let i = indexOf.get(key);
1350
+ if (i === void 0) {
1351
+ i = paints.length;
1352
+ indexOf.set(key, i);
1353
+ paints.push(run.stroke !== void 0 ? { fill: run.fill, stroke: run.stroke } : { fill: run.fill });
1354
+ }
1355
+ return i;
1356
+ });
1357
+ return { paints, paintOf };
1358
+ }
1315
1359
  var EM_CACHE_LIMIT = 4096;
1316
1360
  var emCache = /* @__PURE__ */ new Map();
1317
1361
  function emGlyph(d) {
@@ -1323,6 +1367,37 @@ function emGlyph(d) {
1323
1367
  emCache.set(d, p);
1324
1368
  return p;
1325
1369
  }
1370
+ function placedGlyphOutline(g, dx, dy, caller, blankFallback = false) {
1371
+ const d = faceOutlineD(g);
1372
+ if (d === null) {
1373
+ if (!blankFallback && glyphQuad(g) !== null) {
1374
+ const { family, weight, style } = g.resolved.resolved;
1375
+ throw new TextOutlinesError(
1376
+ "no-outlines",
1377
+ family,
1378
+ weight,
1379
+ style,
1380
+ "a fallback face serving some of the text's characters has no outlines registered.",
1381
+ caller
1382
+ );
1383
+ }
1384
+ return null;
1385
+ }
1386
+ const s = g.fontSize;
1387
+ const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
1388
+ const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
1389
+ return transformPath(emboldenGlyph(emGlyph(d), d, g.resolved), m);
1390
+ }
1391
+ function colorGlyphError(run, caller) {
1392
+ return new TextOutlinesError(
1393
+ "color-glyph",
1394
+ run.fontFamily,
1395
+ run.fontWeight,
1396
+ run.fontStyle,
1397
+ "the text holds an emoji, which is drawn in its own colors and has no outline.",
1398
+ caller
1399
+ );
1400
+ }
1326
1401
  function concat(parts) {
1327
1402
  let nc = 0, nk = 0;
1328
1403
  for (const p of parts) {
@@ -1340,20 +1415,26 @@ function concat(parts) {
1340
1415
  }
1341
1416
  return { kind: "polygon", commands, coords, fillRule: "nonzero" };
1342
1417
  }
1343
- function ruleContour(x0, y0, x1, y1) {
1344
- return {
1345
- kind: "polygon",
1346
- commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
1347
- coords: new Float32Array([x0, y0, x1, y0, x1, y1, x0, y1]),
1348
- fillRule: "nonzero"
1418
+
1419
+ // src/textToPath.ts
1420
+ function facesOf(runs) {
1421
+ const out = /* @__PURE__ */ new Map();
1422
+ const add = (family, weight, style) => {
1423
+ out.set(`${family}|${weight}|${style}`, { family, weight, style });
1349
1424
  };
1425
+ for (const r of runs) {
1426
+ add(r.fontFamily, r.fontWeight, r.fontStyle);
1427
+ const { family, weight, style } = resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle).resolved;
1428
+ add(family, weight, style);
1429
+ }
1430
+ return [...out.values()];
1350
1431
  }
1351
- function walkOutlines(data, pose, opts) {
1432
+ function outlineLayoutInput(data, box, opts, caller) {
1352
1433
  const textPose = {
1353
- x: pose.x,
1354
- y: pose.y,
1355
- width: pose.width,
1356
- height: pose.height,
1434
+ x: box.x,
1435
+ y: box.y,
1436
+ width: box.width,
1437
+ height: box.height,
1357
1438
  text: data.text,
1358
1439
  runs: data.runs,
1359
1440
  style: data.style,
@@ -1361,20 +1442,21 @@ function walkOutlines(data, pose, opts) {
1361
1442
  fill: data.fill,
1362
1443
  stroke: data.stroke
1363
1444
  };
1364
- const { runs, opts: layoutOpts } = textPoseLayoutInput(textPose, opts.scale ?? 1);
1365
- for (const run of runs) assertOutlinable(run);
1366
- const paints = [];
1367
- const indexOf = /* @__PURE__ */ new Map();
1368
- const paintOf = runs.map((run) => {
1369
- const key = `${paintKey(run.fill?.paint)}|${strokeKey(run.stroke)}`;
1370
- let i = indexOf.get(key);
1371
- if (i === void 0) {
1372
- i = paints.length;
1373
- indexOf.set(key, i);
1374
- paints.push(run.stroke !== void 0 ? { fill: run.fill, stroke: run.stroke } : { fill: run.fill });
1375
- }
1376
- return i;
1377
- });
1445
+ const input = textPoseLayoutInput(textPose, opts.scale ?? 1);
1446
+ for (const run of input.runs) assertOutlinable(run, caller);
1447
+ return input;
1448
+ }
1449
+ function ruleContour(x0, y0, x1, y1) {
1450
+ return {
1451
+ kind: "polygon",
1452
+ commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
1453
+ coords: new Float32Array([x0, y0, x1, y0, x1, y1, x0, y1]),
1454
+ fillRule: "nonzero"
1455
+ };
1456
+ }
1457
+ function walkOutlines(data, pose, opts) {
1458
+ const { runs, opts: layoutOpts } = outlineLayoutInput(data, pose, opts, "textToPath");
1459
+ const { paints, paintOf } = paintsOf(runs);
1378
1460
  const laid = layoutRuns(runs, layoutOpts);
1379
1461
  const dx = pose.x;
1380
1462
  const dy = pose.y + verticalAlignOffset(data.verticalAlign, pose.height, laid.bounds.height);
@@ -1383,34 +1465,9 @@ function walkOutlines(data, pose, opts) {
1383
1465
  const faceOf = runFaces(runs);
1384
1466
  for (const line of laid.lines) {
1385
1467
  for (const g of line.glyphs) {
1386
- if (g.glyph?.color) {
1387
- const { fontFamily, fontWeight, fontStyle } = runs[g.run];
1388
- throw new TextOutlinesError(
1389
- "color-glyph",
1390
- fontFamily,
1391
- fontWeight,
1392
- fontStyle,
1393
- "the text holds an emoji, which is drawn in its own colors and has no outline."
1394
- );
1395
- }
1396
- const d = faceOutlineD(g);
1397
- if (d === null) {
1398
- if (glyphQuad(g) !== null) {
1399
- const { family, weight, style } = g.resolved.resolved;
1400
- throw new TextOutlinesError(
1401
- "no-outlines",
1402
- family,
1403
- weight,
1404
- style,
1405
- "a fallback face serving some of the text's characters has no outlines registered."
1406
- );
1407
- }
1408
- continue;
1409
- }
1410
- const s = g.fontSize;
1411
- const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
1412
- const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
1413
- parts[paintOf[g.run]].push(transformPath(emboldenGlyph(emGlyph(d), d, g.resolved), m));
1468
+ if (g.glyph?.color) throw colorGlyphError(runs[g.run], "textToPath");
1469
+ const glyph = placedGlyphOutline(g, dx, dy, "textToPath");
1470
+ if (glyph) parts[paintOf[g.run]].push(glyph);
1414
1471
  }
1415
1472
  for (const r of lineDecorations(line.glyphs, runs, faceOf, (run) => String(paintOf[run]))) {
1416
1473
  rules[paintOf[r.run]].push(ruleContour(r.x0 + dx, r.y0 + dy, r.x1 + dx, r.y1 + dy));
@@ -1446,7 +1503,48 @@ async function loadTextOutlines(data, opts = {}) {
1446
1503
  await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
1447
1504
  await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
1448
1505
  }
1506
+ var MIDDLE_FALLBACK = 0.25;
1507
+ function textClusters(data, opts = {}) {
1508
+ const box = { x: 0, y: 0, width: opts.width ?? Infinity, height: 0 };
1509
+ const { runs, opts: layoutOpts } = outlineLayoutInput(data, box, opts, "textClusters");
1510
+ const line = layoutRuns(runs, layoutOpts).lines[0];
1511
+ if (!line || line.cells.length === 0) return [];
1512
+ const { paints, paintOf } = paintsOf(runs);
1513
+ const glyphOf = new Array(line.cells.length);
1514
+ for (const g of line.glyphs) glyphOf[g.cell] = g;
1515
+ const groups = [];
1516
+ let open = [];
1517
+ line.cells.forEach((cell, i) => {
1518
+ open.push(glyphOf[i]);
1519
+ if (cell.clusterEnd || i === line.cells.length - 1) {
1520
+ groups.push(open);
1521
+ open = [];
1522
+ }
1523
+ });
1524
+ const slots = groups.map((glyphs) => ({ glyphs, left: Math.min(...glyphs.map((g) => g.x)) })).sort((a, b) => a.left - b.left);
1525
+ return slots.map(({ glyphs, left }, i) => {
1526
+ const right = i + 1 < slots.length ? slots[i + 1].left : line.x1;
1527
+ const parts = paints.map(() => []);
1528
+ for (const g of glyphs) {
1529
+ if (g.glyph?.color || !line.cells[g.cell].drawsInk) continue;
1530
+ const outline = placedGlyphOutline(g, -left, -line.baselineY, "textClusters", true);
1531
+ if (outline) parts[paintOf[g.run]].push(outline);
1532
+ }
1533
+ const paths = [];
1534
+ parts.forEach((p, k) => {
1535
+ if (p.length > 0) paths.push({ ...paints[k], path: concat(p) });
1536
+ });
1537
+ const first = glyphs[0];
1538
+ const xHeight = faceMetricsOf(first.resolved)?.xHeight;
1539
+ return {
1540
+ x: left - line.x0,
1541
+ advance: right - left,
1542
+ paths,
1543
+ middle: (xHeight === void 0 ? MIDDLE_FALLBACK : xHeight / 2) * first.fontSize
1544
+ };
1545
+ });
1546
+ }
1449
1547
 
1450
- export { DEFAULT_DECORATION_METRICS, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, SYNTHETIC_ITALIC_RADIANS, TextOutlinesError, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, fitTextPose, isBoldWeight, isSmallCapsLetter, isTextData, layoutMarkdown, layoutTextPose, loadTextOutlines, markdownToRuns, measureText, measureTextBounds, measuredWidth, nodeHasFlag, numericWeight, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, resolveRunFace, resolveRuns, runsCarryStyling, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, setFlagOverRange, smallCapsScale, smallCapsScaleFor, smallCapsText, styleAtRange, supersededKey, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, toRuns, transformRunTexts, unboldPatch, verticalAlignOffset };
1548
+ export { DEFAULT_DECORATION_METRICS, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, SYNTHETIC_ITALIC_RADIANS, TextOutlinesError, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, emboldenGlyphD, fitTextPose, isBoldWeight, isSmallCapsLetter, isTextData, layoutMarkdown, layoutTextPose, loadTextOutlines, markdownToRuns, measureText, measureTextBounds, measuredWidth, nodeHasFlag, numericWeight, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, resolveRunFace, resolveRuns, runsCarryStyling, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, setFlagOverRange, smallCapsScale, smallCapsScaleFor, smallCapsText, styleAtRange, supersededKey, syntheticBoldOffset, textClusters, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, toRuns, transformRunTexts, unboldPatch, verticalAlignOffset };
1451
1549
  //# sourceMappingURL=index.js.map
1452
1550
  //# sourceMappingURL=index.js.map