@mlightcad/cad-pdf-plugin 1.8.0-preview.3 → 1.8.0-preview.4

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.
Files changed (189) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +280 -186
  3. package/dist/cad-pdf-plugin-register.js +29 -11
  4. package/dist/cad-pdf-plugin-register.umd.cjs +1 -1
  5. package/dist/cad-pdf-plugin.js +17568 -8428
  6. package/dist/cad-pdf-plugin.umd.cjs +847 -13
  7. package/lib/AcApConvertToPdfCmd.d.ts.map +1 -1
  8. package/lib/AcApConvertToPdfCmd.js +14 -3
  9. package/lib/AcApConvertToPdfCmd.js.map +1 -1
  10. package/lib/AcApImportPdfCmd.d.ts +5 -3
  11. package/lib/AcApImportPdfCmd.d.ts.map +1 -1
  12. package/lib/AcApImportPdfCmd.js +5 -3
  13. package/lib/AcApImportPdfCmd.js.map +1 -1
  14. package/lib/AcApImportPdfPromptCmd.d.ts +16 -0
  15. package/lib/AcApImportPdfPromptCmd.d.ts.map +1 -0
  16. package/lib/AcApImportPdfPromptCmd.js +89 -0
  17. package/lib/AcApImportPdfPromptCmd.js.map +1 -0
  18. package/lib/AcApPdfBlockCmd.d.ts +15 -0
  19. package/lib/AcApPdfBlockCmd.d.ts.map +1 -0
  20. package/lib/AcApPdfBlockCmd.js +75 -0
  21. package/lib/AcApPdfBlockCmd.js.map +1 -0
  22. package/lib/AcApPdfDimCmd.d.ts +20 -0
  23. package/lib/AcApPdfDimCmd.d.ts.map +1 -0
  24. package/lib/AcApPdfDimCmd.js +413 -0
  25. package/lib/AcApPdfDimCmd.js.map +1 -0
  26. package/lib/AcApPdfDimSession.d.ts +82 -0
  27. package/lib/AcApPdfDimSession.d.ts.map +1 -0
  28. package/lib/AcApPdfDimSession.js +252 -0
  29. package/lib/AcApPdfDimSession.js.map +1 -0
  30. package/lib/AcApPdfFontResolver.d.ts.map +1 -1
  31. package/lib/AcApPdfFontResolver.js +2 -5
  32. package/lib/AcApPdfFontResolver.js.map +1 -1
  33. package/lib/AcApPdfHatchCmd.d.ts +33 -0
  34. package/lib/AcApPdfHatchCmd.d.ts.map +1 -0
  35. package/lib/AcApPdfHatchCmd.js +411 -0
  36. package/lib/AcApPdfHatchCmd.js.map +1 -0
  37. package/lib/AcApPdfHatchSession.d.ts +122 -0
  38. package/lib/AcApPdfHatchSession.d.ts.map +1 -0
  39. package/lib/AcApPdfHatchSession.js +357 -0
  40. package/lib/AcApPdfHatchSession.js.map +1 -0
  41. package/lib/AcApPdfImportConvertor.d.ts +47 -5
  42. package/lib/AcApPdfImportConvertor.d.ts.map +1 -1
  43. package/lib/AcApPdfImportConvertor.js +999 -108
  44. package/lib/AcApPdfImportConvertor.js.map +1 -1
  45. package/lib/AcApPdfImportOptions.d.ts +116 -0
  46. package/lib/AcApPdfImportOptions.d.ts.map +1 -0
  47. package/lib/AcApPdfImportOptions.js +31 -0
  48. package/lib/AcApPdfImportOptions.js.map +1 -0
  49. package/lib/AcApPdfImportResources.d.ts +41 -0
  50. package/lib/AcApPdfImportResources.d.ts.map +1 -0
  51. package/lib/AcApPdfImportResources.js +52 -0
  52. package/lib/AcApPdfImportResources.js.map +1 -0
  53. package/lib/AcApPdfPlugin.d.ts +3 -3
  54. package/lib/AcApPdfPlugin.d.ts.map +1 -1
  55. package/lib/AcApPdfPlugin.js +23 -5
  56. package/lib/AcApPdfPlugin.js.map +1 -1
  57. package/lib/AcApPdfScaleCmd.d.ts +22 -0
  58. package/lib/AcApPdfScaleCmd.d.ts.map +1 -0
  59. package/lib/AcApPdfScaleCmd.js +185 -0
  60. package/lib/AcApPdfScaleCmd.js.map +1 -0
  61. package/lib/AcApPdfTextCmd.d.ts +73 -0
  62. package/lib/AcApPdfTextCmd.d.ts.map +1 -0
  63. package/lib/AcApPdfTextCmd.js +777 -0
  64. package/lib/AcApPdfTextCmd.js.map +1 -0
  65. package/lib/AcApPdfTextSession.d.ts +232 -0
  66. package/lib/AcApPdfTextSession.d.ts.map +1 -0
  67. package/lib/AcApPdfTextSession.js +826 -0
  68. package/lib/AcApPdfTextSession.js.map +1 -0
  69. package/lib/acapPdfI18n.d.ts +11 -0
  70. package/lib/acapPdfI18n.d.ts.map +1 -0
  71. package/lib/acapPdfI18n.js +57 -0
  72. package/lib/acapPdfI18n.js.map +1 -0
  73. package/lib/acapPdfPrompts.d.ts +12 -0
  74. package/lib/acapPdfPrompts.d.ts.map +1 -0
  75. package/lib/acapPdfPrompts.js +100 -0
  76. package/lib/acapPdfPrompts.js.map +1 -0
  77. package/lib/index.d.ts +28 -0
  78. package/lib/index.d.ts.map +1 -1
  79. package/lib/index.js +28 -0
  80. package/lib/index.js.map +1 -1
  81. package/lib/pdfDeprecatedMatching.d.ts +27 -0
  82. package/lib/pdfDeprecatedMatching.d.ts.map +1 -0
  83. package/lib/pdfDeprecatedMatching.js +20 -0
  84. package/lib/pdfDeprecatedMatching.js.map +1 -0
  85. package/lib/pdfDimStrokeTexts.d.ts +55 -0
  86. package/lib/pdfDimStrokeTexts.d.ts.map +1 -0
  87. package/lib/pdfDimStrokeTexts.js +274 -0
  88. package/lib/pdfDimStrokeTexts.js.map +1 -0
  89. package/lib/pdfDimensionConvert.d.ts +80 -0
  90. package/lib/pdfDimensionConvert.d.ts.map +1 -0
  91. package/lib/pdfDimensionConvert.js +566 -0
  92. package/lib/pdfDimensionConvert.js.map +1 -0
  93. package/lib/pdfEntityStrokes.d.ts +13 -0
  94. package/lib/pdfEntityStrokes.d.ts.map +1 -0
  95. package/lib/pdfEntityStrokes.js +86 -0
  96. package/lib/pdfEntityStrokes.js.map +1 -0
  97. package/lib/pdfHatchRecover.d.ts +57 -0
  98. package/lib/pdfHatchRecover.d.ts.map +1 -0
  99. package/lib/pdfHatchRecover.js +315 -0
  100. package/lib/pdfHatchRecover.js.map +1 -0
  101. package/lib/pdfImportAppend.d.ts +24 -0
  102. package/lib/pdfImportAppend.d.ts.map +1 -0
  103. package/lib/pdfImportAppend.js +35 -0
  104. package/lib/pdfImportAppend.js.map +1 -0
  105. package/lib/pdfImportEntities.d.ts +63 -0
  106. package/lib/pdfImportEntities.d.ts.map +1 -0
  107. package/lib/pdfImportEntities.js +307 -0
  108. package/lib/pdfImportEntities.js.map +1 -0
  109. package/lib/pdfImportImages.d.ts +90 -0
  110. package/lib/pdfImportImages.d.ts.map +1 -0
  111. package/lib/pdfImportImages.js +294 -0
  112. package/lib/pdfImportImages.js.map +1 -0
  113. package/lib/pdfImportShapes.d.ts +63 -0
  114. package/lib/pdfImportShapes.d.ts.map +1 -0
  115. package/lib/pdfImportShapes.js +248 -0
  116. package/lib/pdfImportShapes.js.map +1 -0
  117. package/lib/pdfVectorImport.d.ts +218 -5
  118. package/lib/pdfVectorImport.d.ts.map +1 -1
  119. package/lib/pdfVectorImport.js +566 -98
  120. package/lib/pdfVectorImport.js.map +1 -1
  121. package/lib/recognition/pdfCurveRecognition.d.ts +91 -0
  122. package/lib/recognition/pdfCurveRecognition.d.ts.map +1 -0
  123. package/lib/recognition/pdfCurveRecognition.js +506 -0
  124. package/lib/recognition/pdfCurveRecognition.js.map +1 -0
  125. package/lib/recognition/pdfDimDigitRecognition.d.ts +18 -0
  126. package/lib/recognition/pdfDimDigitRecognition.d.ts.map +1 -0
  127. package/lib/recognition/pdfDimDigitRecognition.js +520 -0
  128. package/lib/recognition/pdfDimDigitRecognition.js.map +1 -0
  129. package/lib/recognition/pdfDimensionPattern.d.ts +173 -0
  130. package/lib/recognition/pdfDimensionPattern.d.ts.map +1 -0
  131. package/lib/recognition/pdfDimensionPattern.js +1023 -0
  132. package/lib/recognition/pdfDimensionPattern.js.map +1 -0
  133. package/lib/recognition/pdfFormBlocks.d.ts +58 -0
  134. package/lib/recognition/pdfFormBlocks.d.ts.map +1 -0
  135. package/lib/recognition/pdfFormBlocks.js +260 -0
  136. package/lib/recognition/pdfFormBlocks.js.map +1 -0
  137. package/lib/recognition/pdfGlyphClustering.d.ts +81 -0
  138. package/lib/recognition/pdfGlyphClustering.d.ts.map +1 -0
  139. package/lib/recognition/pdfGlyphClustering.js +1005 -0
  140. package/lib/recognition/pdfGlyphClustering.js.map +1 -0
  141. package/lib/recognition/pdfGlyphDictionary.d.ts +49 -0
  142. package/lib/recognition/pdfGlyphDictionary.d.ts.map +1 -0
  143. package/lib/recognition/pdfGlyphDictionary.js +188 -0
  144. package/lib/recognition/pdfGlyphDictionary.js.map +1 -0
  145. package/lib/recognition/pdfHatchFaces.d.ts +84 -0
  146. package/lib/recognition/pdfHatchFaces.d.ts.map +1 -0
  147. package/lib/recognition/pdfHatchFaces.js +1285 -0
  148. package/lib/recognition/pdfHatchFaces.js.map +1 -0
  149. package/lib/recognition/pdfHatchRecognition.d.ts +222 -0
  150. package/lib/recognition/pdfHatchRecognition.d.ts.map +1 -0
  151. package/lib/recognition/pdfHatchRecognition.js +3781 -0
  152. package/lib/recognition/pdfHatchRecognition.js.map +1 -0
  153. package/lib/recognition/pdfRecGeometry.d.ts +67 -0
  154. package/lib/recognition/pdfRecGeometry.d.ts.map +1 -0
  155. package/lib/recognition/pdfRecGeometry.js +365 -0
  156. package/lib/recognition/pdfRecGeometry.js.map +1 -0
  157. package/lib/recognition/pdfTextFontKind.d.ts +16 -0
  158. package/lib/recognition/pdfTextFontKind.d.ts.map +1 -0
  159. package/lib/recognition/pdfTextFontKind.js +51 -0
  160. package/lib/recognition/pdfTextFontKind.js.map +1 -0
  161. package/lib/recognition/pdfTextMerge.d.ts +71 -0
  162. package/lib/recognition/pdfTextMerge.d.ts.map +1 -0
  163. package/lib/recognition/pdfTextMerge.js +382 -0
  164. package/lib/recognition/pdfTextMerge.js.map +1 -0
  165. package/lib/recognition/pdfTextRunMerge.d.ts +28 -0
  166. package/lib/recognition/pdfTextRunMerge.d.ts.map +1 -0
  167. package/lib/recognition/pdfTextRunMerge.js +90 -0
  168. package/lib/recognition/pdfTextRunMerge.js.map +1 -0
  169. package/lib/register.d.ts +8 -2
  170. package/lib/register.d.ts.map +1 -1
  171. package/lib/register.js +29 -4
  172. package/lib/register.js.map +1 -1
  173. package/lib/ui/AcUiPdfDimPanel.d.ts +55 -0
  174. package/lib/ui/AcUiPdfDimPanel.d.ts.map +1 -0
  175. package/lib/ui/AcUiPdfDimPanel.js +347 -0
  176. package/lib/ui/AcUiPdfDimPanel.js.map +1 -0
  177. package/lib/ui/AcUiPdfHatchPanel.d.ts +57 -0
  178. package/lib/ui/AcUiPdfHatchPanel.d.ts.map +1 -0
  179. package/lib/ui/AcUiPdfHatchPanel.js +420 -0
  180. package/lib/ui/AcUiPdfHatchPanel.js.map +1 -0
  181. package/lib/ui/AcUiPdfImportDialog.d.ts +120 -0
  182. package/lib/ui/AcUiPdfImportDialog.d.ts.map +1 -0
  183. package/lib/ui/AcUiPdfImportDialog.js +673 -0
  184. package/lib/ui/AcUiPdfImportDialog.js.map +1 -0
  185. package/lib/ui/AcUiPdfTextPanel.d.ts +95 -0
  186. package/lib/ui/AcUiPdfTextPanel.d.ts.map +1 -0
  187. package/lib/ui/AcUiPdfTextPanel.js +775 -0
  188. package/lib/ui/AcUiPdfTextPanel.js.map +1 -0
  189. package/package.json +11 -11
@@ -0,0 +1,1005 @@
1
+ /**
2
+ * Groups exploded text strokes (SHX line text, outlined fonts) into text
3
+ * line candidates with a CAD placement.
4
+ *
5
+ * Pure geometry: strokes are plain point lists, so the algorithm is
6
+ * unit-testable without the data model. The `pdftext` command samples
7
+ * entities into strokes and turns candidates into `AcDbText`.
8
+ */
9
+ var __values = (this && this.__values) || function(o) {
10
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
11
+ if (m) return m.call(o);
12
+ if (o && typeof o.length === "number") return {
13
+ next: function () {
14
+ if (o && i >= o.length) o = void 0;
15
+ return { value: o && o[i++], done: !o };
16
+ }
17
+ };
18
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
19
+ };
20
+ var __read = (this && this.__read) || function (o, n) {
21
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
22
+ if (!m) return o;
23
+ var i = m.call(o), r, ar = [], e;
24
+ try {
25
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
26
+ }
27
+ catch (error) { e = { error: error }; }
28
+ finally {
29
+ try {
30
+ if (r && !r.done && (m = i["return"])) m.call(i);
31
+ }
32
+ finally { if (e) throw e.error; }
33
+ }
34
+ return ar;
35
+ };
36
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
37
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
38
+ if (ar || !(i in from)) {
39
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
40
+ ar[i] = from[i];
41
+ }
42
+ }
43
+ return to.concat(ar || Array.prototype.slice.call(from));
44
+ };
45
+ function strokeBox(stroke) {
46
+ var e_1, _a;
47
+ var minX = Infinity;
48
+ var minY = Infinity;
49
+ var maxX = -Infinity;
50
+ var maxY = -Infinity;
51
+ try {
52
+ for (var _b = __values(stroke.points), _c = _b.next(); !_c.done; _c = _b.next()) {
53
+ var p = _c.value;
54
+ minX = Math.min(minX, p.x);
55
+ minY = Math.min(minY, p.y);
56
+ maxX = Math.max(maxX, p.x);
57
+ maxY = Math.max(maxY, p.y);
58
+ }
59
+ }
60
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
61
+ finally {
62
+ try {
63
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
64
+ }
65
+ finally { if (e_1) throw e_1.error; }
66
+ }
67
+ return { minX: minX, minY: minY, maxX: maxX, maxY: maxY };
68
+ }
69
+ function boxSize(box) {
70
+ return Math.max(box.maxX - box.minX, box.maxY - box.minY);
71
+ }
72
+ function boxGap(a, b) {
73
+ var dx = Math.max(0, a.minX - b.maxX, b.minX - a.maxX);
74
+ var dy = Math.max(0, a.minY - b.maxY, b.minY - a.maxY);
75
+ return Math.hypot(dx, dy);
76
+ }
77
+ function percentile(sorted, q) {
78
+ if (sorted.length === 0)
79
+ return 0;
80
+ var index = Math.min(sorted.length - 1, Math.max(0, Math.round(q * (sorted.length - 1))));
81
+ return sorted[index];
82
+ }
83
+ /**
84
+ * Estimates text height from the sizes of glyph parts. Parts far above the
85
+ * median are treated as non-text so a frame in the selection does not skew
86
+ * the result.
87
+ *
88
+ * Strokes chained end to end are measured together: some PDFs store a glyph
89
+ * stroke as many tiny separate segments, each far smaller than the text.
90
+ */
91
+ export function estimatePdfTextHeight(strokes) {
92
+ var sized = strokes.filter(function (stroke) { return boxSize(strokeBox(stroke)) > 0; });
93
+ if (sized.length === 0)
94
+ return 0;
95
+ var strokeMedian = percentile(sized.map(function (stroke) { return boxSize(strokeBox(stroke)); }).sort(byNumber), 0.5);
96
+ var sizes = chainStrokes(sized, strokeMedian * 0.01)
97
+ .map(function (group) {
98
+ return boxSize(strokeBox({ id: 0, points: group.flatMap(function (i) { return sized[i].points; }) }));
99
+ })
100
+ .sort(byNumber);
101
+ var median = percentile(sizes, 0.5);
102
+ var textLike = sizes.filter(function (size) { return size <= median * 4; });
103
+ return percentile(textLike, 0.9);
104
+ }
105
+ function byNumber(a, b) {
106
+ return a - b;
107
+ }
108
+ /**
109
+ * Groups strokes whose end points meet within `tolerance` (a stroke split
110
+ * into consecutive segments), using a hash grid of end points.
111
+ *
112
+ * @param canJoin - Optional extra condition for joining two strokes.
113
+ */
114
+ function chainStrokes(strokes, tolerance, canJoin) {
115
+ var uf = new UnionFind(strokes.length);
116
+ var cell = Math.max(tolerance, 1e-12);
117
+ var grid = new Map();
118
+ strokes.forEach(function (stroke, index) {
119
+ var e_2, _a, e_3, _b;
120
+ var _c;
121
+ var ends = [stroke.points[0], stroke.points[stroke.points.length - 1]];
122
+ try {
123
+ for (var ends_1 = __values(ends), ends_1_1 = ends_1.next(); !ends_1_1.done; ends_1_1 = ends_1.next()) {
124
+ var p = ends_1_1.value;
125
+ var gx = Math.floor(p.x / cell);
126
+ var gy = Math.floor(p.y / cell);
127
+ for (var dx = -1; dx <= 1; dx++) {
128
+ for (var dy = -1; dy <= 1; dy++) {
129
+ try {
130
+ for (var _d = (e_3 = void 0, __values((_c = grid.get("".concat(gx + dx, ",").concat(gy + dy))) !== null && _c !== void 0 ? _c : [])), _e = _d.next(); !_e.done; _e = _d.next()) {
131
+ var other = _e.value;
132
+ if (Math.hypot(other.p.x - p.x, other.p.y - p.y) <= tolerance &&
133
+ (!canJoin || canJoin(index, other.index))) {
134
+ uf.union(index, other.index);
135
+ }
136
+ }
137
+ }
138
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
139
+ finally {
140
+ try {
141
+ if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
142
+ }
143
+ finally { if (e_3) throw e_3.error; }
144
+ }
145
+ }
146
+ }
147
+ var key = "".concat(gx, ",").concat(gy);
148
+ var bucket = grid.get(key);
149
+ if (bucket)
150
+ bucket.push({ index: index, p: p });
151
+ else
152
+ grid.set(key, [{ index: index, p: p }]);
153
+ }
154
+ }
155
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
156
+ finally {
157
+ try {
158
+ if (ends_1_1 && !ends_1_1.done && (_a = ends_1.return)) _a.call(ends_1);
159
+ }
160
+ finally { if (e_2) throw e_2.error; }
161
+ }
162
+ });
163
+ var groups = new Map();
164
+ strokes.forEach(function (_, i) {
165
+ var root = uf.find(i);
166
+ var group = groups.get(root);
167
+ if (group)
168
+ group.push(i);
169
+ else
170
+ groups.set(root, [i]);
171
+ });
172
+ return __spreadArray([], __read(groups.values()), false);
173
+ }
174
+ /** Sine of the largest angle between segments still treated as collinear. */
175
+ var COLLINEAR_SINE = Math.sin((2 * Math.PI) / 180);
176
+ /** Unit direction of a straight stroke, or `undefined` for a curved one. */
177
+ function straightDirection(stroke, tolerance) {
178
+ var e_4, _a;
179
+ var a = stroke.points[0];
180
+ var b = stroke.points[stroke.points.length - 1];
181
+ var length = Math.hypot(b.x - a.x, b.y - a.y);
182
+ if (!(length > 0))
183
+ return undefined;
184
+ var ux = (b.x - a.x) / length;
185
+ var uy = (b.y - a.y) / length;
186
+ try {
187
+ for (var _b = __values(stroke.points), _c = _b.next(); !_c.done; _c = _b.next()) {
188
+ var p = _c.value;
189
+ if (Math.abs((p.x - a.x) * uy - (p.y - a.y) * ux) > tolerance) {
190
+ return undefined;
191
+ }
192
+ }
193
+ }
194
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
195
+ finally {
196
+ try {
197
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
198
+ }
199
+ finally { if (e_4) throw e_4.error; }
200
+ }
201
+ return { x: ux, y: uy };
202
+ }
203
+ /**
204
+ * Size of the straight line each stroke belongs to: straight strokes that
205
+ * meet end to end in the same direction are measured together. PDFs often
206
+ * split table grids and frames into segments shorter than the text, which
207
+ * would otherwise pass as glyph strokes and glue table cells together.
208
+ */
209
+ function straightRunSizes(strokes, tolerance) {
210
+ var e_5, _a;
211
+ var directions = strokes.map(function (stroke) { return straightDirection(stroke, tolerance); });
212
+ var runs = chainStrokes(strokes, tolerance, function (i, j) {
213
+ var a = directions[i];
214
+ var b = directions[j];
215
+ return !!a && !!b && Math.abs(a.x * b.y - a.y * b.x) <= COLLINEAR_SINE;
216
+ });
217
+ var sizes = new Array(strokes.length);
218
+ var _loop_1 = function (run) {
219
+ var size = boxSize(strokeBox({ id: 0, points: run.flatMap(function (i) { return strokes[i].points; }) }));
220
+ run.forEach(function (i) { return (sizes[i] = size); });
221
+ };
222
+ try {
223
+ for (var runs_1 = __values(runs), runs_1_1 = runs_1.next(); !runs_1_1.done; runs_1_1 = runs_1.next()) {
224
+ var run = runs_1_1.value;
225
+ _loop_1(run);
226
+ }
227
+ }
228
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
229
+ finally {
230
+ try {
231
+ if (runs_1_1 && !runs_1_1.done && (_a = runs_1.return)) _a.call(runs_1);
232
+ }
233
+ finally { if (e_5) throw e_5.error; }
234
+ }
235
+ return sizes;
236
+ }
237
+ var UnionFind = /** @class */ (function () {
238
+ function UnionFind(n) {
239
+ this.parent = Array.from({ length: n }, function (_, i) { return i; });
240
+ }
241
+ UnionFind.prototype.find = function (i) {
242
+ while (this.parent[i] !== i) {
243
+ this.parent[i] = this.parent[this.parent[i]];
244
+ i = this.parent[i];
245
+ }
246
+ return i;
247
+ };
248
+ UnionFind.prototype.union = function (a, b) {
249
+ var ra = this.find(a);
250
+ var rb = this.find(b);
251
+ if (ra !== rb)
252
+ this.parent[ra] = rb;
253
+ };
254
+ return UnionFind;
255
+ }());
256
+ /** Joins items whose boxes are within `gap`, using a uniform grid. */
257
+ function clusterBoxes(boxes, gap) {
258
+ var uf = new UnionFind(boxes.length);
259
+ var cell = Math.max(gap * 2, 1e-9);
260
+ var grid = new Map();
261
+ boxes.forEach(function (box, i) {
262
+ var e_6, _a;
263
+ var x0 = Math.floor((box.minX - gap) / cell);
264
+ var x1 = Math.floor((box.maxX + gap) / cell);
265
+ var y0 = Math.floor((box.minY - gap) / cell);
266
+ var y1 = Math.floor((box.maxY + gap) / cell);
267
+ var seen = new Set();
268
+ for (var gx = x0; gx <= x1; gx++) {
269
+ for (var gy = y0; gy <= y1; gy++) {
270
+ var key = "".concat(gx, ",").concat(gy);
271
+ var bucket = grid.get(key);
272
+ if (bucket) {
273
+ try {
274
+ for (var bucket_1 = (e_6 = void 0, __values(bucket)), bucket_1_1 = bucket_1.next(); !bucket_1_1.done; bucket_1_1 = bucket_1.next()) {
275
+ var j = bucket_1_1.value;
276
+ if (seen.has(j))
277
+ continue;
278
+ seen.add(j);
279
+ if (boxGap(box, boxes[j]) <= gap)
280
+ uf.union(i, j);
281
+ }
282
+ }
283
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
284
+ finally {
285
+ try {
286
+ if (bucket_1_1 && !bucket_1_1.done && (_a = bucket_1.return)) _a.call(bucket_1);
287
+ }
288
+ finally { if (e_6) throw e_6.error; }
289
+ }
290
+ bucket.push(i);
291
+ }
292
+ else {
293
+ grid.set(key, [i]);
294
+ }
295
+ }
296
+ }
297
+ });
298
+ var groups = new Map();
299
+ boxes.forEach(function (_, i) {
300
+ var root = uf.find(i);
301
+ var group = groups.get(root);
302
+ if (group)
303
+ group.push(i);
304
+ else
305
+ groups.set(root, [i]);
306
+ });
307
+ return __spreadArray([], __read(groups.values()), false);
308
+ }
309
+ /**
310
+ * Baseline direction of a text cluster, in `(-π/2, π/2]`.
311
+ *
312
+ * Uses the direction of the minimum-height oriented bounding box (rotating
313
+ * calipers over the convex hull): along a text line the extent across the
314
+ * baseline is smallest. Principal axes would be skewed by asymmetric glyphs.
315
+ * Returns `undefined` when the cluster is not elongated enough to tell.
316
+ */
317
+ function principalAngle(points) {
318
+ var e_7, _a;
319
+ var hull = convexHull(points);
320
+ if (hull.length < 2)
321
+ return undefined;
322
+ var bestAngle = 0;
323
+ var bestHeight = Infinity;
324
+ var bestLength = 0;
325
+ for (var i = 0; i < hull.length; i++) {
326
+ var a = hull[i];
327
+ var b = hull[(i + 1) % hull.length];
328
+ var len = Math.hypot(b.x - a.x, b.y - a.y);
329
+ if (len === 0)
330
+ continue;
331
+ var ux = (b.x - a.x) / len;
332
+ var uy = (b.y - a.y) / len;
333
+ var minU = Infinity;
334
+ var maxU = -Infinity;
335
+ var minV = Infinity;
336
+ var maxV = -Infinity;
337
+ try {
338
+ for (var hull_1 = (e_7 = void 0, __values(hull)), hull_1_1 = hull_1.next(); !hull_1_1.done; hull_1_1 = hull_1.next()) {
339
+ var p = hull_1_1.value;
340
+ var u = p.x * ux + p.y * uy;
341
+ var v = -p.x * uy + p.y * ux;
342
+ minU = Math.min(minU, u);
343
+ maxU = Math.max(maxU, u);
344
+ minV = Math.min(minV, v);
345
+ maxV = Math.max(maxV, v);
346
+ }
347
+ }
348
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
349
+ finally {
350
+ try {
351
+ if (hull_1_1 && !hull_1_1.done && (_a = hull_1.return)) _a.call(hull_1);
352
+ }
353
+ finally { if (e_7) throw e_7.error; }
354
+ }
355
+ var height = maxV - minV;
356
+ if (height < bestHeight - 1e-12) {
357
+ bestHeight = height;
358
+ bestLength = maxU - minU;
359
+ bestAngle = Math.atan2(uy, ux);
360
+ }
361
+ }
362
+ // A single glyph or a square block of text has no reliable direction.
363
+ if (!(bestLength > bestHeight * 1.8))
364
+ return undefined;
365
+ return normalizeTextAngle(bestAngle);
366
+ }
367
+ /** Fewest glyph blobs for {@link neighborAngle} to vote on a direction. */
368
+ var NEIGHBOR_MIN_BLOBS = 3;
369
+ /** Width of one direction histogram bin, in radians. */
370
+ var NEIGHBOR_BIN = Math.PI / 180;
371
+ /**
372
+ * Baseline direction of a phrase from the direction between each glyph and
373
+ * its nearest neighbor, in `(-π/2, π/2]`.
374
+ *
375
+ * Characters of a line are closer to each other than lines are, so the most
376
+ * common nearest-neighbor direction is the baseline. Unlike the phrase
377
+ * outline, this also works for blocks taller than wide: a narrow column of
378
+ * table cells or a paragraph of short lines.
379
+ *
380
+ * @returns The direction, or `undefined` when the phrase has fewer than
381
+ * {@link NEIGHBOR_MIN_BLOBS} glyph blobs.
382
+ */
383
+ function neighborAngle(strokes, textHeight, wordGap) {
384
+ var e_8, _a;
385
+ // Blobs: touching strokes, roughly one glyph or glyph part each.
386
+ var boxes = strokes.map(strokeBox);
387
+ var blobs = clusterBoxes(boxes, textHeight * 0.02).map(function (group) {
388
+ var e_9, _a;
389
+ var minX = Infinity;
390
+ var minY = Infinity;
391
+ var maxX = -Infinity;
392
+ var maxY = -Infinity;
393
+ try {
394
+ for (var group_1 = __values(group), group_1_1 = group_1.next(); !group_1_1.done; group_1_1 = group_1.next()) {
395
+ var i = group_1_1.value;
396
+ minX = Math.min(minX, boxes[i].minX);
397
+ minY = Math.min(minY, boxes[i].minY);
398
+ maxX = Math.max(maxX, boxes[i].maxX);
399
+ maxY = Math.max(maxY, boxes[i].maxY);
400
+ }
401
+ }
402
+ catch (e_9_1) { e_9 = { error: e_9_1 }; }
403
+ finally {
404
+ try {
405
+ if (group_1_1 && !group_1_1.done && (_a = group_1.return)) _a.call(group_1);
406
+ }
407
+ finally { if (e_9) throw e_9.error; }
408
+ }
409
+ return { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
410
+ });
411
+ if (blobs.length < NEIGHBOR_MIN_BLOBS)
412
+ return undefined;
413
+ var cell = Math.max(textHeight, 1e-9);
414
+ var grid = new Map();
415
+ blobs.forEach(function (blob, i) {
416
+ var key = "".concat(Math.floor(blob.x / cell), ",").concat(Math.floor(blob.y / cell));
417
+ var bucket = grid.get(key);
418
+ if (bucket)
419
+ bucket.push(i);
420
+ else
421
+ grid.set(key, [i]);
422
+ });
423
+ var reach = Math.ceil(Math.max(wordGap, 1));
424
+ var bins = Math.round(Math.PI / NEIGHBOR_BIN);
425
+ var votes = new Array(bins).fill(0);
426
+ var angles = [];
427
+ blobs.forEach(function (blob, i) {
428
+ var e_10, _a;
429
+ var _b;
430
+ var gx = Math.floor(blob.x / cell);
431
+ var gy = Math.floor(blob.y / cell);
432
+ var best = -1;
433
+ var bestDistance = Infinity;
434
+ for (var dx = -reach; dx <= reach; dx++) {
435
+ for (var dy = -reach; dy <= reach; dy++) {
436
+ try {
437
+ for (var _c = (e_10 = void 0, __values((_b = grid.get("".concat(gx + dx, ",").concat(gy + dy))) !== null && _b !== void 0 ? _b : [])), _d = _c.next(); !_d.done; _d = _c.next()) {
438
+ var j = _d.value;
439
+ if (j === i)
440
+ continue;
441
+ var d = Math.hypot(blobs[j].x - blob.x, blobs[j].y - blob.y);
442
+ if (d > 0 && d < bestDistance) {
443
+ bestDistance = d;
444
+ best = j;
445
+ }
446
+ }
447
+ }
448
+ catch (e_10_1) { e_10 = { error: e_10_1 }; }
449
+ finally {
450
+ try {
451
+ if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
452
+ }
453
+ finally { if (e_10) throw e_10.error; }
454
+ }
455
+ }
456
+ }
457
+ if (best < 0)
458
+ return;
459
+ var angle = Math.atan2(blobs[best].y - blob.y, blobs[best].x - blob.x);
460
+ if (angle < 0)
461
+ angle += Math.PI;
462
+ if (angle >= Math.PI)
463
+ angle -= Math.PI;
464
+ angles.push(angle);
465
+ votes[Math.min(bins - 1, Math.floor(angle / NEIGHBOR_BIN))]++;
466
+ });
467
+ if (angles.length === 0)
468
+ return undefined;
469
+ // Mode of the histogram, smoothed over neighboring bins (circular).
470
+ var bestBin = 0;
471
+ var bestScore = -1;
472
+ for (var b = 0; b < bins; b++) {
473
+ var score = 0;
474
+ for (var k = -2; k <= 2; k++)
475
+ score += votes[(b + k + bins) % bins];
476
+ if (score > bestScore) {
477
+ bestScore = score;
478
+ bestBin = b;
479
+ }
480
+ }
481
+ // Refine: mean of the votes near the mode, unwrapped around it.
482
+ var center = (bestBin + 0.5) * NEIGHBOR_BIN;
483
+ var sum = 0;
484
+ var count = 0;
485
+ try {
486
+ for (var angles_1 = __values(angles), angles_1_1 = angles_1.next(); !angles_1_1.done; angles_1_1 = angles_1.next()) {
487
+ var angle = angles_1_1.value;
488
+ var delta = angle - center;
489
+ if (delta > Math.PI / 2)
490
+ delta -= Math.PI;
491
+ if (delta < -Math.PI / 2)
492
+ delta += Math.PI;
493
+ if (Math.abs(delta) <= 3 * NEIGHBOR_BIN) {
494
+ sum += delta;
495
+ count++;
496
+ }
497
+ }
498
+ }
499
+ catch (e_8_1) { e_8 = { error: e_8_1 }; }
500
+ finally {
501
+ try {
502
+ if (angles_1_1 && !angles_1_1.done && (_a = angles_1.return)) _a.call(angles_1);
503
+ }
504
+ finally { if (e_8) throw e_8.error; }
505
+ }
506
+ return normalizeTextAngle(center + (count > 0 ? sum / count : 0));
507
+ }
508
+ /** Candidates scoring within this fraction of the best count as a tie. */
509
+ var ANGLE_TIE = 0.05;
510
+ /**
511
+ * How much better a tilted direction must score than 0° / 90° to be used:
512
+ * drawings are mostly axis-aligned, and a few glyphs (`Q1`) can make a
513
+ * slight tilt score marginally better.
514
+ */
515
+ var TILT_MARGIN = 0.2;
516
+ /** How much better than every other direction a clear direction scores. */
517
+ var ANGLE_CLEAR = 0.25;
518
+ /** Lines shorter than this many times their thickness are not real lines. */
519
+ var MIN_LINE_ASPECT = 1.5;
520
+ /**
521
+ * Baseline direction of a phrase: among 0°, 90°, the nearest-neighbor vote
522
+ * and the outline direction, the one whose lines have the tightest glyph
523
+ * spacing.
524
+ *
525
+ * Along the baseline, glyphs of a line are close together. Read across it,
526
+ * "lines" are columns of characters whose gaps are the line spacing, or
527
+ * single glyphs / single characters (not elongated) that count as a word
528
+ * gap. This holds for single lines, paragraphs, widely spaced titles and
529
+ * narrow table columns alike, where the phrase outline or nearest neighbors
530
+ * (often parts of one character) fail. Ties keep the earlier candidate, so
531
+ * horizontal text stays exactly horizontal.
532
+ *
533
+ * @returns The direction, and whether it is clear. It is not when the
534
+ * phrase reads as fewer than three glyphs, or no direction scores clearly
535
+ * better than the others: short words like `Q1` look the same as one
536
+ * character read sideways, so only the rest of the drawing can tell.
537
+ */
538
+ function bestPhraseAngle(strokes, textHeight, wordGap, glyphGap) {
539
+ var e_11, _a;
540
+ var candidates = [
541
+ 0,
542
+ Math.PI / 2,
543
+ neighborAngle(strokes, textHeight, wordGap),
544
+ principalAngle(strokes.flatMap(function (stroke) { return stroke.points; }))
545
+ ].filter(function (angle, i, all) {
546
+ return angle !== undefined &&
547
+ all
548
+ .slice(0, i)
549
+ .every(function (other) {
550
+ return other === undefined ||
551
+ Math.abs(normalizeTextAngle(other) - normalizeTextAngle(angle)) >
552
+ NEIGHBOR_BIN;
553
+ });
554
+ });
555
+ var best = candidates[0];
556
+ var bestScore = Infinity;
557
+ var bestGlyphs = 0;
558
+ var scores = [];
559
+ var _loop_2 = function (angle) {
560
+ var e_12, _b, e_13, _c;
561
+ var gaps = 0;
562
+ var count = 0;
563
+ var glyphCount = 0;
564
+ var local = localStrokes(strokes, angle);
565
+ try {
566
+ for (var _d = (e_12 = void 0, __values(splitByGaps(local, 'v', textHeight * 0.1))), _e = _d.next(); !_e.done; _e = _d.next()) {
567
+ var row = _e.value;
568
+ var _loop_3 = function (line) {
569
+ var glyphs = splitByGaps(line, 'u', glyphGap * textHeight);
570
+ glyphCount += glyphs.length;
571
+ // One glyph, or one character read sideways (its stacked parts),
572
+ // counts as a word gap.
573
+ var band = lineBand(line);
574
+ if (glyphs.length < 2 ||
575
+ band.maxU - band.minU < (band.maxV - band.minV) * MIN_LINE_ASPECT) {
576
+ gaps += wordGap * textHeight;
577
+ count++;
578
+ return "continue";
579
+ }
580
+ var reach = -Infinity;
581
+ glyphs.forEach(function (glyph, i) {
582
+ var minU = Math.min.apply(Math, __spreadArray([], __read(glyph.map(function (item) { return item.minU; })), false));
583
+ if (i > 0) {
584
+ gaps += minU - reach;
585
+ count++;
586
+ }
587
+ reach = Math.max.apply(Math, __spreadArray([], __read(glyph.map(function (item) { return item.maxU; })), false));
588
+ });
589
+ };
590
+ try {
591
+ for (var _f = (e_13 = void 0, __values(splitByGaps(row, 'u', wordGap * textHeight))), _g = _f.next(); !_g.done; _g = _f.next()) {
592
+ var line = _g.value;
593
+ _loop_3(line);
594
+ }
595
+ }
596
+ catch (e_13_1) { e_13 = { error: e_13_1 }; }
597
+ finally {
598
+ try {
599
+ if (_g && !_g.done && (_c = _f.return)) _c.call(_f);
600
+ }
601
+ finally { if (e_13) throw e_13.error; }
602
+ }
603
+ }
604
+ }
605
+ catch (e_12_1) { e_12 = { error: e_12_1 }; }
606
+ finally {
607
+ try {
608
+ if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
609
+ }
610
+ finally { if (e_12) throw e_12.error; }
611
+ }
612
+ var score = count > 0 ? gaps / count : Infinity;
613
+ scores.push(score);
614
+ var axis = Math.abs(Math.sin(2 * angle)) < 1e-9;
615
+ if (score < bestScore * (1 - (axis ? ANGLE_TIE : TILT_MARGIN))) {
616
+ best = angle;
617
+ bestScore = score;
618
+ bestGlyphs = glyphCount;
619
+ }
620
+ };
621
+ try {
622
+ for (var candidates_1 = __values(candidates), candidates_1_1 = candidates_1.next(); !candidates_1_1.done; candidates_1_1 = candidates_1.next()) {
623
+ var angle = candidates_1_1.value;
624
+ _loop_2(angle);
625
+ }
626
+ }
627
+ catch (e_11_1) { e_11 = { error: e_11_1 }; }
628
+ finally {
629
+ try {
630
+ if (candidates_1_1 && !candidates_1_1.done && (_a = candidates_1.return)) _a.call(candidates_1);
631
+ }
632
+ finally { if (e_11) throw e_11.error; }
633
+ }
634
+ var runnerUp = Math.min.apply(Math, __spreadArray(__spreadArray([], __read(scores.filter(function (_, i) { return candidates[i] !== best; })), false), [Infinity], false));
635
+ return {
636
+ angle: normalizeTextAngle(best),
637
+ // Two glyphs cannot tell a line from a column.
638
+ clear: bestGlyphs >= 3 && bestScore <= runnerUp * (1 - ANGLE_CLEAR)
639
+ };
640
+ }
641
+ /** Andrew's monotone chain convex hull (counterclockwise). */
642
+ function convexHull(points) {
643
+ var e_14, _a;
644
+ var pts = __spreadArray([], __read(points), false).sort(function (a, b) { return a.x - b.x || a.y - b.y; });
645
+ if (pts.length < 3)
646
+ return pts;
647
+ var cross = function (o, a, b) {
648
+ return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
649
+ };
650
+ var lower = [];
651
+ try {
652
+ for (var pts_1 = __values(pts), pts_1_1 = pts_1.next(); !pts_1_1.done; pts_1_1 = pts_1.next()) {
653
+ var p = pts_1_1.value;
654
+ while (lower.length >= 2 &&
655
+ cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
656
+ lower.pop();
657
+ }
658
+ lower.push(p);
659
+ }
660
+ }
661
+ catch (e_14_1) { e_14 = { error: e_14_1 }; }
662
+ finally {
663
+ try {
664
+ if (pts_1_1 && !pts_1_1.done && (_a = pts_1.return)) _a.call(pts_1);
665
+ }
666
+ finally { if (e_14) throw e_14.error; }
667
+ }
668
+ var upper = [];
669
+ for (var i = pts.length - 1; i >= 0; i--) {
670
+ var p = pts[i];
671
+ while (upper.length >= 2 &&
672
+ cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
673
+ upper.pop();
674
+ }
675
+ upper.push(p);
676
+ }
677
+ lower.pop();
678
+ upper.pop();
679
+ return lower.concat(upper);
680
+ }
681
+ /** Folds an angle into `(-π/2, π/2]` and snaps near-orthogonal angles. */
682
+ function normalizeTextAngle(angle) {
683
+ var a = angle;
684
+ while (a <= -Math.PI / 2)
685
+ a += Math.PI;
686
+ while (a > Math.PI / 2)
687
+ a -= Math.PI;
688
+ var snapped = Math.round(a / (Math.PI / 2)) * (Math.PI / 2);
689
+ if (Math.abs(a - snapped) < (2 * Math.PI) / 180)
690
+ a = snapped;
691
+ if (a <= -Math.PI / 2 + 1e-12)
692
+ a += Math.PI;
693
+ return a;
694
+ }
695
+ function toLocal(p, angle) {
696
+ var c = Math.cos(angle);
697
+ var s = Math.sin(angle);
698
+ return { u: p.x * c + p.y * s, v: -p.x * s + p.y * c };
699
+ }
700
+ function toWorld(u, v, angle) {
701
+ var c = Math.cos(angle);
702
+ var s = Math.sin(angle);
703
+ return { x: u * c - v * s, y: u * s + v * c };
704
+ }
705
+ function localStrokes(strokes, angle) {
706
+ return strokes.map(function (stroke) {
707
+ var e_15, _a;
708
+ var minU = Infinity;
709
+ var maxU = -Infinity;
710
+ var minV = Infinity;
711
+ var maxV = -Infinity;
712
+ try {
713
+ for (var _b = __values(stroke.points), _c = _b.next(); !_c.done; _c = _b.next()) {
714
+ var p = _c.value;
715
+ var _d = toLocal(p, angle), u = _d.u, v = _d.v;
716
+ minU = Math.min(minU, u);
717
+ maxU = Math.max(maxU, u);
718
+ minV = Math.min(minV, v);
719
+ maxV = Math.max(maxV, v);
720
+ }
721
+ }
722
+ catch (e_15_1) { e_15 = { error: e_15_1 }; }
723
+ finally {
724
+ try {
725
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
726
+ }
727
+ finally { if (e_15) throw e_15.error; }
728
+ }
729
+ return { stroke: stroke, minU: minU, maxU: maxU, minV: minV, maxV: maxV };
730
+ });
731
+ }
732
+ /** Splits strokes into groups separated by gaps along one local axis. */
733
+ function splitByGaps(items, axis, gap) {
734
+ var e_16, _a;
735
+ var lo = axis === 'u' ? 'minU' : 'minV';
736
+ var hi = axis === 'u' ? 'maxU' : 'maxV';
737
+ var sorted = __spreadArray([], __read(items), false).sort(function (a, b) { return a[lo] - b[lo]; });
738
+ var groups = [];
739
+ var current = [];
740
+ var reach = -Infinity;
741
+ try {
742
+ for (var sorted_1 = __values(sorted), sorted_1_1 = sorted_1.next(); !sorted_1_1.done; sorted_1_1 = sorted_1.next()) {
743
+ var item = sorted_1_1.value;
744
+ if (current.length > 0 && item[lo] - reach > gap) {
745
+ groups.push(current);
746
+ current = [];
747
+ reach = -Infinity;
748
+ }
749
+ current.push(item);
750
+ reach = Math.max(reach, item[hi]);
751
+ }
752
+ }
753
+ catch (e_16_1) { e_16 = { error: e_16_1 }; }
754
+ finally {
755
+ try {
756
+ if (sorted_1_1 && !sorted_1_1.done && (_a = sorted_1.return)) _a.call(sorted_1);
757
+ }
758
+ finally { if (e_16) throw e_16.error; }
759
+ }
760
+ if (current.length > 0)
761
+ groups.push(current);
762
+ return groups;
763
+ }
764
+ function median(values) {
765
+ var sorted = __spreadArray([], __read(values), false).sort(function (a, b) { return a - b; });
766
+ return percentile(sorted, 0.5);
767
+ }
768
+ function lineBand(items) {
769
+ return {
770
+ minU: Math.min.apply(Math, __spreadArray([], __read(items.map(function (item) { return item.minU; })), false)),
771
+ maxU: Math.max.apply(Math, __spreadArray([], __read(items.map(function (item) { return item.maxU; })), false)),
772
+ minV: Math.min.apply(Math, __spreadArray([], __read(items.map(function (item) { return item.minV; })), false)),
773
+ maxV: Math.max.apply(Math, __spreadArray([], __read(items.map(function (item) { return item.maxV; })), false))
774
+ };
775
+ }
776
+ /**
777
+ * Whether a stroke fits a text line: inside its band (with a quarter-height
778
+ * margin) and no larger than about the line height.
779
+ */
780
+ function fitsBand(item, band) {
781
+ var height = band.maxV - band.minV;
782
+ var margin = height * 0.25;
783
+ var size = Math.max(item.maxU - item.minU, item.maxV - item.minV);
784
+ return (size <= height * 1.5 &&
785
+ item.minU >= band.minU - margin &&
786
+ item.maxU <= band.maxU + margin &&
787
+ item.minV >= band.minV - margin &&
788
+ item.maxV <= band.maxV + margin);
789
+ }
790
+ /**
791
+ * Whether a rejected stroke can join a text line taller than the estimated
792
+ * height: across the baseline inside its band (quarter-height margin), along
793
+ * it within a word gap, and no larger than about the line height.
794
+ *
795
+ * @param size - Size to check: the full straight line a segment belongs to,
796
+ * so pieces of grid lines next to text are not attached.
797
+ */
798
+ function joinsBand(item, band, size, wordGap) {
799
+ var height = band.maxV - band.minV;
800
+ var margin = height * 0.25;
801
+ var gap = height * wordGap;
802
+ return (size <= height * 1.5 &&
803
+ item.maxU >= band.minU - gap &&
804
+ item.minU <= band.maxU + gap &&
805
+ item.minV >= band.minV - margin &&
806
+ item.maxV <= band.maxV + margin);
807
+ }
808
+ function extendBand(band, item) {
809
+ band.minU = Math.min(band.minU, item.minU);
810
+ band.maxU = Math.max(band.maxU, item.maxU);
811
+ band.minV = Math.min(band.minV, item.minV);
812
+ band.maxV = Math.max(band.maxV, item.maxV);
813
+ }
814
+ /**
815
+ * Merges rows lying inside the band of a taller row into it: pieces of a
816
+ * large glyph that were split off where a long stroke, rejected at first,
817
+ * joined them to the rest.
818
+ */
819
+ function mergeFragments(rows) {
820
+ var height = function (row) {
821
+ var band = lineBand(row.items);
822
+ return band.maxV - band.minV;
823
+ };
824
+ var order = __spreadArray([], __read(rows), false).sort(function (a, b) { return height(a) - height(b); });
825
+ var removed = new Set();
826
+ order.forEach(function (row, i) {
827
+ var _a;
828
+ var strokes = row.items.map(function (item) { return item.stroke; });
829
+ for (var j = order.length - 1; j > i; j--) {
830
+ var target = order[j];
831
+ if (!(height(target) > height(row)))
832
+ continue;
833
+ var local = localStrokes(strokes, target.angle);
834
+ if (!fitsBand(lineBand(local), lineBand(target.items)))
835
+ continue;
836
+ (_a = target.items).push.apply(_a, __spreadArray([], __read(local), false));
837
+ removed.add(row);
838
+ return;
839
+ }
840
+ });
841
+ return rows.filter(function (row) { return !removed.has(row); });
842
+ }
843
+ function buildLine(items, angle, textHeight, glyphGap) {
844
+ var glyphs = splitByGaps(items, 'u', glyphGap * textHeight).map(function (group) { return ({
845
+ strokes: group.map(function (item) { return item.stroke; }),
846
+ minU: Math.min.apply(Math, __spreadArray([], __read(group.map(function (item) { return item.minU; })), false)),
847
+ maxU: Math.max.apply(Math, __spreadArray([], __read(group.map(function (item) { return item.maxU; })), false)),
848
+ minV: Math.min.apply(Math, __spreadArray([], __read(group.map(function (item) { return item.minV; })), false)),
849
+ maxV: Math.max.apply(Math, __spreadArray([], __read(group.map(function (item) { return item.maxV; })), false))
850
+ }); });
851
+ // Descenders (g, p, y) hang below the baseline: most glyphs sit on it.
852
+ var baseline = median(glyphs.map(function (glyph) { return glyph.minV; }));
853
+ var tops = glyphs.map(function (glyph) { return glyph.maxV; }).sort(function (a, b) { return a - b; });
854
+ var top = percentile(tops, 0.9);
855
+ var minU = Math.min.apply(Math, __spreadArray([], __read(glyphs.map(function (glyph) { return glyph.minU; })), false));
856
+ var maxU = Math.max.apply(Math, __spreadArray([], __read(glyphs.map(function (glyph) { return glyph.maxU; })), false));
857
+ return {
858
+ strokes: items.map(function (item) { return item.stroke; }),
859
+ glyphs: glyphs,
860
+ rotation: angle,
861
+ position: toWorld(minU, baseline, angle),
862
+ height: Math.max(top - baseline, 1e-9),
863
+ width: maxU - minU
864
+ };
865
+ }
866
+ /**
867
+ * Clusters strokes into text line candidates.
868
+ *
869
+ * 1. Strokes much larger than text are rejected.
870
+ * 2. Strokes closer than `wordGap · height` join into phrases.
871
+ * 3. Each phrase gets a baseline direction (minimum-height bounding box,
872
+ * or the dominant direction when the phrase is a single glyph).
873
+ * 4. Each phrase is split into rows at gaps across the baseline, and rows
874
+ * into lines at gaps along it wider than `wordGap · height`.
875
+ *
876
+ * @param strokes - Candidate strokes in world coordinates.
877
+ * @param options - Height hint and gap factors.
878
+ */
879
+ export function clusterPdfTextStrokes(strokes, options) {
880
+ var _a, _b, _c;
881
+ if (options === void 0) { options = {}; }
882
+ var usable = strokes.filter(function (stroke) { return stroke.points.length > 0; });
883
+ var textHeight = (_a = options.textHeight) !== null && _a !== void 0 ? _a : estimatePdfTextHeight(usable);
884
+ var wordGap = (_b = options.wordGap) !== null && _b !== void 0 ? _b : 1.2;
885
+ var glyphGap = (_c = options.glyphGap) !== null && _c !== void 0 ? _c : 0.04;
886
+ if (!(textHeight > 0)) {
887
+ return { lines: [], rejected: usable, textHeight: 0 };
888
+ }
889
+ // Straight lines split into short segments (table grids, frames) are
890
+ // judged by their full length; long strokes of a taller title rejected
891
+ // here are attached back to their line below.
892
+ var runSizes = straightRunSizes(usable, textHeight * 0.01);
893
+ var accepted = [];
894
+ var rejected = [];
895
+ var rejectedSize = new Map();
896
+ usable.forEach(function (stroke, i) {
897
+ var size = Math.max(boxSize(strokeBox(stroke)), runSizes[i]);
898
+ if (size > textHeight * 1.5) {
899
+ rejected.push(stroke);
900
+ rejectedSize.set(stroke, size);
901
+ }
902
+ else {
903
+ accepted.push(stroke);
904
+ }
905
+ });
906
+ // Phrases: strokes within a word gap. Lines of a paragraph may join into
907
+ // one phrase; they are split into rows below once the direction is known.
908
+ var boxes = accepted.map(strokeBox);
909
+ var phrases = clusterBoxes(boxes, wordGap * textHeight).map(function (group) {
910
+ return group.map(function (i) { return accepted[i]; });
911
+ });
912
+ // Direction per phrase: the candidate with the tightest glyph spacing.
913
+ // Phrases too small or too ambiguous to tell (one glyph, `Q1`, or a title
914
+ // whose long strokes are attached only later) borrow the dominant
915
+ // direction of the clear ones.
916
+ var judged = phrases.map(function (phrase) {
917
+ return boxSize(strokeBox({ id: 0, points: phrase.flatMap(function (s) { return s.points; }) })) <
918
+ textHeight * 2
919
+ ? undefined
920
+ : bestPhraseAngle(phrase, textHeight, wordGap, glyphGap);
921
+ });
922
+ var known = judged.flatMap(function (result) { return ((result === null || result === void 0 ? void 0 : result.clear) ? [result.angle] : []); });
923
+ var dominant = known.length > 0 ? median(known) : undefined;
924
+ var angles = phrases.map(function (phrase, i) {
925
+ var _a;
926
+ var result = judged[i];
927
+ if (result === null || result === void 0 ? void 0 : result.clear)
928
+ return result.angle;
929
+ return ((_a = dominant !== null && dominant !== void 0 ? dominant : result === null || result === void 0 ? void 0 : result.angle) !== null && _a !== void 0 ? _a : principalAngle(phrase.flatMap(function (stroke) { return stroke.points; })));
930
+ });
931
+ var merged = [];
932
+ phrases.forEach(function (phrase, i) {
933
+ var e_17, _a, e_18, _b;
934
+ var _c;
935
+ var angle = (_c = angles[i]) !== null && _c !== void 0 ? _c : 0;
936
+ var local = localStrokes(phrase, angle);
937
+ try {
938
+ // Rows: gaps across the baseline. Lines: word gaps along it.
939
+ for (var _d = __values(splitByGaps(local, 'v', textHeight * 0.1)), _e = _d.next(); !_e.done; _e = _d.next()) {
940
+ var row = _e.value;
941
+ try {
942
+ for (var _f = (e_18 = void 0, __values(splitByGaps(row, 'u', wordGap * textHeight))), _g = _f.next(); !_g.done; _g = _f.next()) {
943
+ var line = _g.value;
944
+ merged.push({ angle: angle, items: line });
945
+ }
946
+ }
947
+ catch (e_18_1) { e_18 = { error: e_18_1 }; }
948
+ finally {
949
+ try {
950
+ if (_g && !_g.done && (_b = _f.return)) _b.call(_f);
951
+ }
952
+ finally { if (e_18) throw e_18.error; }
953
+ }
954
+ }
955
+ }
956
+ catch (e_17_1) { e_17 = { error: e_17_1 }; }
957
+ finally {
958
+ try {
959
+ if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
960
+ }
961
+ finally { if (e_17) throw e_17.error; }
962
+ }
963
+ });
964
+ // Strokes too long for the estimated height may belong to taller text (a
965
+ // title above body text): attach those lying in the band of such a line.
966
+ // Bands grow as strokes join, so repeat until nothing changes.
967
+ var bands = merged.map(function (row) { return lineBand(row.items); });
968
+ var stillRejected = rejected;
969
+ var _loop_4 = function (changed) {
970
+ changed = false;
971
+ stillRejected = stillRejected.filter(function (stroke) {
972
+ var _a;
973
+ var size = (_a = rejectedSize.get(stroke)) !== null && _a !== void 0 ? _a : 0;
974
+ for (var i = 0; i < merged.length; i++) {
975
+ var item = localStrokes([stroke], merged[i].angle)[0];
976
+ if (!joinsBand(item, bands[i], size, wordGap))
977
+ continue;
978
+ merged[i].items.push(item);
979
+ extendBand(bands[i], item);
980
+ changed = true;
981
+ return false;
982
+ }
983
+ return true;
984
+ });
985
+ out_changed_1 = changed;
986
+ };
987
+ var out_changed_1;
988
+ for (var changed = true; changed && stillRejected.length > 0;) {
989
+ _loop_4(changed);
990
+ changed = out_changed_1;
991
+ }
992
+ var rows = mergeFragments(merged);
993
+ var lines = rows.map(function (row) {
994
+ return buildLine(row.items, row.angle, textHeight, glyphGap);
995
+ });
996
+ // Reading order: top to bottom, then left to right.
997
+ lines.sort(function (a, b) {
998
+ var dy = b.position.y - a.position.y;
999
+ if (Math.abs(dy) > Math.min(a.height, b.height) * 0.5)
1000
+ return dy;
1001
+ return a.position.x - b.position.x;
1002
+ });
1003
+ return { lines: lines, rejected: stillRejected, textHeight: textHeight };
1004
+ }
1005
+ //# sourceMappingURL=pdfGlyphClustering.js.map