Skip to content

packages/text/src/lib/bounds.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import type { TextGeometryResult } from '../index.js';
2 import type { PathGeometry } from "@pibbl/core";
3 /**
4  * Axis-aligned bounds of visible glyph outlines in logical output coordinates.
5  *
6  * @see {@link TextGeometryResult}
7  */
8 export interface InkBounds {
9   /**
10    * Horizontal coordinate or displacement in the containing coordinate system. See
11    * {@link InkBounds}.
12    */
13   readonly x: number;
14   /**
15    * Vertical coordinate or displacement in the containing coordinate system. See {@link InkBounds}
16    * .
17    */
18   readonly y: number;
19   /**
20    * Horizontal extent in the units of the containing geometry or surface. See {@link InkBounds}.
21    */
22   readonly width: number;
23   /** Vertical extent in the units of the containing geometry or surface. See {@link InkBounds}. */
24   readonly height: number;
25 }
26 /** Curve extrema, not control-point bounds. One linear traversal; no flattening. */
27 export function inkBounds(path: PathGeometry): Readonly<InkBounds> | null {
28   let minX = Infinity,
29     minY = Infinity,
30     maxX = -Infinity,
31     maxY = -Infinity;
32   let x = 0,
33     y = 0,
34     startX = 0,
35     startY = 0;
36   const include = (a: number, b: number) => {
37     minX = Math.min(minX, a);
38     maxX = Math.max(maxX, a);
39     minY = Math.min(minY, b);
40     maxY = Math.max(maxY, b);
41   };
42   const cubicRoots = (p0: number, p1: number, p2: number, p3: number) => {
43     // Normalize first to keep coefficient arithmetic finite for large coordinates.
44     const scale = Math.max(
45       Math.abs(p0),
46       Math.abs(p1),
47       Math.abs(p2),
48       Math.abs(p3),
49       1,
50     );
51     p0 /= scale;
52     p1 /= scale;
53     p2 /= scale;
54     p3 /= scale;
55     const a = -p0 + 3 * p1 - 3 * p2 + p3;
56     const b = 2 * (p0 - 2 * p1 + p2),
57       c = p1 - p0;
58     if (a === 0) return b === 0 ? [] : [-c / b];
59     const discriminant = b * b - 4 * a * c;
60     if (discriminant < 0) return [];
61     const q = -0.5 * (b + (b < 0 ? -1 : 1) * Math.sqrt(discriminant));
62     return q === 0 ? [-b / (2 * a)] : [q / a, c / q];
63   };
64   const lerp = (a: number, b: number, t: number) => a * (1 - t) + b * t;
65   const quadratic = (a: number, b: number, c: number, t: number) =>
66     lerp(lerp(a, b, t), lerp(b, c, t), t);
67   const cubic = (a: number, b: number, c: number, d: number, t: number) =>
68     lerp(quadratic(a, b, c, t), quadratic(b, c, d, t), t);
69   for (const segment of path) {
70     if (segment.type === "move") {
71       x = startX = segment.x;
72       y = startY = segment.y;
73       continue;
74     }
75     if (segment.type === "close") {
76       if (x !== startX || y !== startY) {
77         include(x, y);
78         include(startX, startY);
79       }
80       x = startX;
81       y = startY;
82       continue;
83     }
84     if (segment.type === "arc")
85       throw new Error("Font outlines cannot contain analytic arc commands.");
86     include(x, y);
87     include(segment.x, segment.y);
88     if (segment.type === "quadratic") {
89       const root = (a: number, b: number, c: number) => {
90         const scale = Math.max(Math.abs(a), Math.abs(b), Math.abs(c), 1);
91         a /= scale;
92         b /= scale;
93         c /= scale;
94         return (a - b) / (a - 2 * b + c);
95       };
96       for (const t of [
97         root(x, segment.cpx, segment.x),
98         root(y, segment.cpy, segment.y),
99       ]) {
100         if (t > 0 && t < 1)
101           include(
102             quadratic(x, segment.cpx, segment.x, t),
103             quadratic(y, segment.cpy, segment.y, t),
104           );
105       }
106     } else if (segment.type === "cubic") {
107       for (const t of [
108         ...cubicRoots(x, segment.cp1x, segment.cp2x, segment.x),
109         ...cubicRoots(y, segment.cp1y, segment.cp2y, segment.y),
110       ]) {
111         if (t > 0 && t < 1)
112           include(
113             cubic(x, segment.cp1x, segment.cp2x, segment.x, t),
114             cubic(y, segment.cp1y, segment.cp2y, segment.y, t),
115           );
116       }
117     }
118     x = segment.x;
119     y = segment.y;
120   }
121   if (minX === Infinity) return null;
122   const result = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
123   if (!Object.values(result).every(Number.isFinite))
124     throw new RangeError("Outline bounds overflow.");
125   return Object.freeze(result);
126 }
127 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.