postext 1.19.2 → 1.20.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.
Files changed (251) hide show
  1. package/dist/__tests__/comics/art.test.d.ts +2 -0
  2. package/dist/__tests__/comics/art.test.d.ts.map +1 -0
  3. package/dist/__tests__/comics/art.test.js +96 -0
  4. package/dist/__tests__/comics/art.test.js.map +1 -0
  5. package/dist/__tests__/comics/build.test.d.ts +2 -0
  6. package/dist/__tests__/comics/build.test.d.ts.map +1 -0
  7. package/dist/__tests__/comics/build.test.js +215 -0
  8. package/dist/__tests__/comics/build.test.js.map +1 -0
  9. package/dist/__tests__/comics/canvas.test.d.ts +2 -0
  10. package/dist/__tests__/comics/canvas.test.d.ts.map +1 -0
  11. package/dist/__tests__/comics/canvas.test.js +161 -0
  12. package/dist/__tests__/comics/canvas.test.js.map +1 -0
  13. package/dist/__tests__/comics/config.test.d.ts +2 -0
  14. package/dist/__tests__/comics/config.test.d.ts.map +1 -0
  15. package/dist/__tests__/comics/config.test.js +97 -0
  16. package/dist/__tests__/comics/config.test.js.map +1 -0
  17. package/dist/__tests__/comics/geometry.test.d.ts +2 -0
  18. package/dist/__tests__/comics/geometry.test.d.ts.map +1 -0
  19. package/dist/__tests__/comics/geometry.test.js +118 -0
  20. package/dist/__tests__/comics/geometry.test.js.map +1 -0
  21. package/dist/__tests__/comics/html.test.d.ts +2 -0
  22. package/dist/__tests__/comics/html.test.d.ts.map +1 -0
  23. package/dist/__tests__/comics/html.test.js +253 -0
  24. package/dist/__tests__/comics/html.test.js.map +1 -0
  25. package/dist/__tests__/comics/lettering.test.d.ts +2 -0
  26. package/dist/__tests__/comics/lettering.test.d.ts.map +1 -0
  27. package/dist/__tests__/comics/lettering.test.js +241 -0
  28. package/dist/__tests__/comics/lettering.test.js.map +1 -0
  29. package/dist/__tests__/comics/script.test.d.ts +2 -0
  30. package/dist/__tests__/comics/script.test.d.ts.map +1 -0
  31. package/dist/__tests__/comics/script.test.js +147 -0
  32. package/dist/__tests__/comics/script.test.js.map +1 -0
  33. package/dist/__tests__/comics/split.test.d.ts +2 -0
  34. package/dist/__tests__/comics/split.test.d.ts.map +1 -0
  35. package/dist/__tests__/comics/split.test.js +120 -0
  36. package/dist/__tests__/comics/split.test.js.map +1 -0
  37. package/dist/__tests__/comics/spread.test.d.ts +2 -0
  38. package/dist/__tests__/comics/spread.test.d.ts.map +1 -0
  39. package/dist/__tests__/comics/spread.test.js +161 -0
  40. package/dist/__tests__/comics/spread.test.js.map +1 -0
  41. package/dist/__tests__/comics/strip.test.d.ts +2 -0
  42. package/dist/__tests__/comics/strip.test.d.ts.map +1 -0
  43. package/dist/__tests__/comics/strip.test.js +231 -0
  44. package/dist/__tests__/comics/strip.test.js.map +1 -0
  45. package/dist/__tests__/exports.test.js +89 -0
  46. package/dist/__tests__/exports.test.js.map +1 -1
  47. package/dist/__tests__/parse/containers.test.js +1 -1
  48. package/dist/__tests__/parse/containers.test.js.map +1 -1
  49. package/dist/canvas-backend/comic.d.ts +32 -0
  50. package/dist/canvas-backend/comic.d.ts.map +1 -0
  51. package/dist/canvas-backend/comic.js +216 -0
  52. package/dist/canvas-backend/comic.js.map +1 -0
  53. package/dist/canvas-backend/headerFooter.d.ts +4 -1
  54. package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
  55. package/dist/canvas-backend/headerFooter.js +3 -1
  56. package/dist/canvas-backend/headerFooter.js.map +1 -1
  57. package/dist/canvas-backend/index.d.ts.map +1 -1
  58. package/dist/canvas-backend/index.js +7 -1
  59. package/dist/canvas-backend/index.js.map +1 -1
  60. package/dist/comics/art.d.ts +72 -0
  61. package/dist/comics/art.d.ts.map +1 -0
  62. package/dist/comics/art.js +154 -0
  63. package/dist/comics/art.js.map +1 -0
  64. package/dist/comics/fonts.d.ts +20 -0
  65. package/dist/comics/fonts.d.ts.map +1 -0
  66. package/dist/comics/fonts.js +42 -0
  67. package/dist/comics/fonts.js.map +1 -0
  68. package/dist/comics/geometry.d.ts +87 -0
  69. package/dist/comics/geometry.d.ts.map +1 -0
  70. package/dist/comics/geometry.js +272 -0
  71. package/dist/comics/geometry.js.map +1 -0
  72. package/dist/comics/index.d.ts +29 -0
  73. package/dist/comics/index.d.ts.map +1 -0
  74. package/dist/comics/index.js +18 -0
  75. package/dist/comics/index.js.map +1 -0
  76. package/dist/comics/layoutPage.d.ts +156 -0
  77. package/dist/comics/layoutPage.d.ts.map +1 -0
  78. package/dist/comics/layoutPage.js +697 -0
  79. package/dist/comics/layoutPage.js.map +1 -0
  80. package/dist/comics/lettering/__tests__/harness.test.d.ts +2 -0
  81. package/dist/comics/lettering/__tests__/harness.test.d.ts.map +1 -0
  82. package/dist/comics/lettering/__tests__/harness.test.js +273 -0
  83. package/dist/comics/lettering/__tests__/harness.test.js.map +1 -0
  84. package/dist/comics/lettering/__tests__/letterPanel.test.d.ts +2 -0
  85. package/dist/comics/lettering/__tests__/letterPanel.test.d.ts.map +1 -0
  86. package/dist/comics/lettering/__tests__/letterPanel.test.js +224 -0
  87. package/dist/comics/lettering/__tests__/letterPanel.test.js.map +1 -0
  88. package/dist/comics/lettering/__tests__/polish.test.d.ts +2 -0
  89. package/dist/comics/lettering/__tests__/polish.test.d.ts.map +1 -0
  90. package/dist/comics/lettering/__tests__/polish.test.js +357 -0
  91. package/dist/comics/lettering/__tests__/polish.test.js.map +1 -0
  92. package/dist/comics/lettering/__tests__/round2.test.d.ts +2 -0
  93. package/dist/comics/lettering/__tests__/round2.test.d.ts.map +1 -0
  94. package/dist/comics/lettering/__tests__/round2.test.js +289 -0
  95. package/dist/comics/lettering/__tests__/round2.test.js.map +1 -0
  96. package/dist/comics/lettering/__tests__/shapeText.test.d.ts +2 -0
  97. package/dist/comics/lettering/__tests__/shapeText.test.d.ts.map +1 -0
  98. package/dist/comics/lettering/__tests__/shapeText.test.js +98 -0
  99. package/dist/comics/lettering/__tests__/shapeText.test.js.map +1 -0
  100. package/dist/comics/lettering/__tests__/shapes.test.d.ts +2 -0
  101. package/dist/comics/lettering/__tests__/shapes.test.d.ts.map +1 -0
  102. package/dist/comics/lettering/__tests__/shapes.test.js +137 -0
  103. package/dist/comics/lettering/__tests__/shapes.test.js.map +1 -0
  104. package/dist/comics/lettering/__tests__/stubMeasure.d.ts +5 -0
  105. package/dist/comics/lettering/__tests__/stubMeasure.d.ts.map +1 -0
  106. package/dist/comics/lettering/__tests__/stubMeasure.js +47 -0
  107. package/dist/comics/lettering/__tests__/stubMeasure.js.map +1 -0
  108. package/dist/comics/lettering/__tests__/text.test.d.ts +2 -0
  109. package/dist/comics/lettering/__tests__/text.test.d.ts.map +1 -0
  110. package/dist/comics/lettering/__tests__/text.test.js +86 -0
  111. package/dist/comics/lettering/__tests__/text.test.js.map +1 -0
  112. package/dist/comics/lettering/geom.d.ts +78 -0
  113. package/dist/comics/lettering/geom.d.ts.map +1 -0
  114. package/dist/comics/lettering/geom.js +376 -0
  115. package/dist/comics/lettering/geom.js.map +1 -0
  116. package/dist/comics/lettering/index.d.ts +7 -0
  117. package/dist/comics/lettering/index.d.ts.map +1 -0
  118. package/dist/comics/lettering/index.js +8 -0
  119. package/dist/comics/lettering/index.js.map +1 -0
  120. package/dist/comics/lettering/letter.d.ts +7 -0
  121. package/dist/comics/lettering/letter.d.ts.map +1 -0
  122. package/dist/comics/lettering/letter.js +397 -0
  123. package/dist/comics/lettering/letter.js.map +1 -0
  124. package/dist/comics/lettering/placement.d.ts +117 -0
  125. package/dist/comics/lettering/placement.d.ts.map +1 -0
  126. package/dist/comics/lettering/placement.js +889 -0
  127. package/dist/comics/lettering/placement.js.map +1 -0
  128. package/dist/comics/lettering/presets.d.ts +15 -0
  129. package/dist/comics/lettering/presets.d.ts.map +1 -0
  130. package/dist/comics/lettering/presets.js +61 -0
  131. package/dist/comics/lettering/presets.js.map +1 -0
  132. package/dist/comics/lettering/shape-text.d.ts +72 -0
  133. package/dist/comics/lettering/shape-text.d.ts.map +1 -0
  134. package/dist/comics/lettering/shape-text.js +550 -0
  135. package/dist/comics/lettering/shape-text.js.map +1 -0
  136. package/dist/comics/lettering/shapes.d.ts +83 -0
  137. package/dist/comics/lettering/shapes.d.ts.map +1 -0
  138. package/dist/comics/lettering/shapes.js +512 -0
  139. package/dist/comics/lettering/shapes.js.map +1 -0
  140. package/dist/comics/lettering/text.d.ts +79 -0
  141. package/dist/comics/lettering/text.d.ts.map +1 -0
  142. package/dist/comics/lettering/text.js +488 -0
  143. package/dist/comics/lettering/text.js.map +1 -0
  144. package/dist/comics/lettering/types.d.ts +270 -0
  145. package/dist/comics/lettering/types.d.ts.map +1 -0
  146. package/dist/comics/lettering/types.js +9 -0
  147. package/dist/comics/lettering/types.js.map +1 -0
  148. package/dist/comics/page.d.ts +37 -0
  149. package/dist/comics/page.d.ts.map +1 -0
  150. package/dist/comics/page.js +285 -0
  151. package/dist/comics/page.js.map +1 -0
  152. package/dist/comics/paint.d.ts +65 -0
  153. package/dist/comics/paint.d.ts.map +1 -0
  154. package/dist/comics/paint.js +219 -0
  155. package/dist/comics/paint.js.map +1 -0
  156. package/dist/comics/script.d.ts +42 -0
  157. package/dist/comics/script.d.ts.map +1 -0
  158. package/dist/comics/script.js +302 -0
  159. package/dist/comics/script.js.map +1 -0
  160. package/dist/comics/split.d.ts +135 -0
  161. package/dist/comics/split.d.ts.map +1 -0
  162. package/dist/comics/split.js +514 -0
  163. package/dist/comics/split.js.map +1 -0
  164. package/dist/comics/spread.d.ts +36 -0
  165. package/dist/comics/spread.d.ts.map +1 -0
  166. package/dist/comics/spread.js +172 -0
  167. package/dist/comics/spread.js.map +1 -0
  168. package/dist/comics/strip.d.ts +77 -0
  169. package/dist/comics/strip.d.ts.map +1 -0
  170. package/dist/comics/strip.js +89 -0
  171. package/dist/comics/strip.js.map +1 -0
  172. package/dist/comics/transform.d.ts +37 -0
  173. package/dist/comics/transform.d.ts.map +1 -0
  174. package/dist/comics/transform.js +161 -0
  175. package/dist/comics/transform.js.map +1 -0
  176. package/dist/comics/types.d.ts +143 -0
  177. package/dist/comics/types.d.ts.map +1 -0
  178. package/dist/comics/types.js +13 -0
  179. package/dist/comics/types.js.map +1 -0
  180. package/dist/comics/warnings.d.ts +14 -0
  181. package/dist/comics/warnings.d.ts.map +1 -0
  182. package/dist/comics/warnings.js +94 -0
  183. package/dist/comics/warnings.js.map +1 -0
  184. package/dist/configWarnings.d.ts.map +1 -1
  185. package/dist/configWarnings.js +42 -0
  186. package/dist/configWarnings.js.map +1 -1
  187. package/dist/defaults/comics.d.ts +51 -0
  188. package/dist/defaults/comics.d.ts.map +1 -0
  189. package/dist/defaults/comics.js +385 -0
  190. package/dist/defaults/comics.js.map +1 -0
  191. package/dist/defaults/index.d.ts +1 -0
  192. package/dist/defaults/index.d.ts.map +1 -1
  193. package/dist/defaults/index.js +9 -0
  194. package/dist/defaults/index.js.map +1 -1
  195. package/dist/design/layout.d.ts +13 -1
  196. package/dist/design/layout.d.ts.map +1 -1
  197. package/dist/design/layout.js +17 -0
  198. package/dist/design/layout.js.map +1 -1
  199. package/dist/html-backend.d.ts +3 -0
  200. package/dist/html-backend.d.ts.map +1 -1
  201. package/dist/html-backend.js +44 -8
  202. package/dist/html-backend.js.map +1 -1
  203. package/dist/htmlComic.d.ts +69 -0
  204. package/dist/htmlComic.d.ts.map +1 -0
  205. package/dist/htmlComic.js +179 -0
  206. package/dist/htmlComic.js.map +1 -0
  207. package/dist/index.d.ts +8 -2
  208. package/dist/index.d.ts.map +1 -1
  209. package/dist/index.js +4 -0
  210. package/dist/index.js.map +1 -1
  211. package/dist/parse/blockParser.d.ts.map +1 -1
  212. package/dist/parse/blockParser.js +25 -1
  213. package/dist/parse/blockParser.js.map +1 -1
  214. package/dist/parse/indexMarks.d.ts.map +1 -1
  215. package/dist/parse/indexMarks.js +3 -0
  216. package/dist/parse/indexMarks.js.map +1 -1
  217. package/dist/parse/types.d.ts +7 -1
  218. package/dist/parse/types.d.ts.map +1 -1
  219. package/dist/pipeline/build.d.ts.map +1 -1
  220. package/dist/pipeline/build.js +251 -9
  221. package/dist/pipeline/build.js.map +1 -1
  222. package/dist/pipeline/config.d.ts.map +1 -1
  223. package/dist/pipeline/config.js +4 -1
  224. package/dist/pipeline/config.js.map +1 -1
  225. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  226. package/dist/pipeline/contentWarnings.js +58 -0
  227. package/dist/pipeline/contentWarnings.js.map +1 -1
  228. package/dist/pipeline/floatSlots.js +1 -1
  229. package/dist/pipeline/floatSlots.js.map +1 -1
  230. package/dist/pipeline/headerFooter.d.ts +6 -2
  231. package/dist/pipeline/headerFooter.d.ts.map +1 -1
  232. package/dist/pipeline/headerFooter.js +11 -3
  233. package/dist/pipeline/headerFooter.js.map +1 -1
  234. package/dist/pipeline/pageRoles.d.ts +1 -0
  235. package/dist/pipeline/pageRoles.d.ts.map +1 -1
  236. package/dist/pipeline/pageRoles.js +3 -0
  237. package/dist/pipeline/pageRoles.js.map +1 -1
  238. package/dist/pipeline/placement.d.ts +3 -3
  239. package/dist/pipeline/placement.d.ts.map +1 -1
  240. package/dist/pipeline/placement.js +4 -4
  241. package/dist/pipeline/placement.js.map +1 -1
  242. package/dist/types.d.ts +338 -1
  243. package/dist/types.d.ts.map +1 -1
  244. package/dist/vdt.d.ts +271 -2
  245. package/dist/vdt.d.ts.map +1 -1
  246. package/dist/vdt.js.map +1 -1
  247. package/dist/worker/fonts.d.ts +2 -1
  248. package/dist/worker/fonts.d.ts.map +1 -1
  249. package/dist/worker/fonts.js +11 -1
  250. package/dist/worker/fonts.js.map +1 -1
  251. package/package.json +1 -1
@@ -0,0 +1,224 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { installStubMeasure } from './stubMeasure';
3
+ import { letterPanel, letterPanelDetailed } from '../letter';
4
+ import { presetLetteringStyles } from '../presets';
5
+ import { dist, pointInRect } from '../geom';
6
+ beforeAll(() => installStubMeasure());
7
+ const EM = 12;
8
+ const rect = (x, y, width, height) => ({ x, y, width, height });
9
+ const corners = (r) => [{ x: r.x, y: r.y }, { x: r.x + r.width, y: r.y }, { x: r.x + r.width, y: r.y + r.height }, { x: r.x, y: r.y + r.height }];
10
+ // One panel, two speakers: ana on the left, ben on the right, a lamp
11
+ // between them that must stay clear.
12
+ const CELL = rect(0, 0, 600, 320);
13
+ const ANA = { id: 'ana', mouth: { x: 150, y: 250 }, head: { x: 150, y: 190 }, face: rect(110, 200, 80, 80), visible: true };
14
+ const BEN = { id: 'ben', mouth: { x: 450, y: 255 }, head: { x: 450, y: 195 }, face: rect(410, 205, 80, 80), visible: true };
15
+ const LAMP = rect(280, 170, 40, 150);
16
+ const LOCS = [
17
+ { tag: 'en', dir: 'ltr', vertical: false },
18
+ { tag: 'es', dir: 'ltr', vertical: false },
19
+ { tag: 'ja', dir: 'rtl', vertical: true },
20
+ { tag: 'ar', dir: 'rtl', vertical: false },
21
+ ];
22
+ const LINES = {
23
+ en: ['Did you hear that? Something is moving in the cellar.', "It's nothing. Go back to sleep.", 'Are you sure?'],
24
+ es: ['¿Has oído eso? Algo se mueve en el sótano.', 'No es nada. Vuelve a dormir.', '¿Estás seguro?'],
25
+ ja: ['いまの音、聞こえた?地下室で何かが動いてる。', 'なんでもないよ。もう寝なさい。', '本当に?'],
26
+ ar: ['هل سمعت ذلك؟ شيء ما يتحرك في القبو.', 'لا شيء. عودي إلى النوم.', 'هل أنت متأكد؟'],
27
+ };
28
+ function panelFor(loc, extra = {}) {
29
+ return {
30
+ index: 0, polygon: corners(CELL), bbox: CELL, insetPx: 6, direction: loc.dir,
31
+ writingMode: loc.vertical ? 'vertical' : 'horizontal', locale: loc.tag, anchors: [ANA, BEN], avoid: [LAMP], dpi: 96,
32
+ ...extra,
33
+ };
34
+ }
35
+ function itemsFor(loc) {
36
+ const st = presetLetteringStyles({ fontSizePx: EM, locale: loc.tag, fontFamily: 'Test' });
37
+ const [a, b, c] = LINES[loc.tag];
38
+ return [
39
+ { id: 'i0', order: 0, kind: 'balloon', speaker: 'ana', text: a, sourceStart: 0, sourceEnd: a.length, style: st.speech },
40
+ { id: 'i1', order: 1, kind: 'balloon', speaker: 'ben', text: b, sourceStart: 100, sourceEnd: 100 + b.length, style: st.speech },
41
+ { id: 'i2', order: 2, kind: 'balloon', speaker: 'ana', text: c, sourceStart: 200, sourceEnd: 200 + c.length, style: st.speech },
42
+ ];
43
+ }
44
+ const overlap = (a, b) => Math.min(a.x + a.width, b.x + b.width) > Math.max(a.x, b.x) && Math.min(a.y + a.height, b.y + b.height) > Math.max(a.y, b.y);
45
+ const shrunk = (r, k) => ({ x: r.x + r.width * k, y: r.y + r.height * k, width: r.width * (1 - 2 * k), height: r.height * (1 - 2 * k) });
46
+ describe('letterPanel: placement (SPEC D3.3)', () => {
47
+ for (const loc of LOCS) {
48
+ describe(loc.tag, () => {
49
+ let out;
50
+ beforeAll(() => {
51
+ out = letterPanel(panelFor(loc), itemsFor(loc));
52
+ });
53
+ it('gives one balloon per item, in reading order, with real design text', () => {
54
+ expect(out.map((b) => b.id)).toEqual(['i0', 'i1', 'i2']);
55
+ for (const b of out) {
56
+ expect(b.text).toHaveLength(1);
57
+ expect(b.text[0].kind).toBe('text');
58
+ expect(b.text[0].lines.length).toBeGreaterThan(0);
59
+ if (loc.vertical)
60
+ expect(b.text[0].vertical).toBeDefined();
61
+ if (loc.tag === 'ar')
62
+ expect(b.text[0].direction).toBe('rtl');
63
+ }
64
+ });
65
+ it('keeps the balloons inside the panel and apart', () => {
66
+ for (const b of out) {
67
+ expect(b.bbox.x).toBeGreaterThanOrEqual(CELL.x);
68
+ expect(b.bbox.y).toBeGreaterThanOrEqual(CELL.y);
69
+ expect(b.bbox.x + b.bbox.width).toBeLessThanOrEqual(CELL.x + CELL.width);
70
+ expect(b.bbox.y + b.bbox.height).toBeLessThanOrEqual(CELL.y + CELL.height);
71
+ }
72
+ for (let i = 0; i < out.length; i++) {
73
+ for (let j = i + 1; j < out.length; j++)
74
+ expect(overlap(shrunk(out[i].bbox, 0.15), shrunk(out[j].bbox, 0.15))).toBe(false);
75
+ }
76
+ });
77
+ it('never covers a face or the lamp', () => {
78
+ for (const b of out) {
79
+ for (const r of [ANA.face, BEN.face, LAMP])
80
+ expect(overlap(shrunk(b.bbox, 0.12), r)).toBe(false);
81
+ for (const t of b.text)
82
+ expect(overlap(t.bbox, ANA.face) || overlap(t.bbox, BEN.face)).toBe(false);
83
+ }
84
+ });
85
+ it('aims every tail at its own speaker', () => {
86
+ for (const b of out) {
87
+ expect(b.tailTip).toBeDefined();
88
+ const own = b.speaker === 'ana' ? ANA.mouth : BEN.mouth;
89
+ const other = b.speaker === 'ana' ? BEN.mouth : ANA.mouth;
90
+ expect(dist(b.tailTip, own)).toBeLessThan(dist(b.tailTip, other));
91
+ expect(b.shape.d.startsWith('M')).toBe(true);
92
+ }
93
+ });
94
+ it('respects the reading order (Kurlander, mirrored right to left)', () => {
95
+ const fwd = loc.dir === 'rtl' ? -1 : 1;
96
+ for (let i = 0; i < out.length; i++) {
97
+ for (let j = i + 1; j < out.length; j++) {
98
+ const e = out[i].bbox;
99
+ const l = out[j].bbox;
100
+ const earlierForward = ((e.x + e.width / 2) - (l.x + l.width / 2)) * fwd > 0;
101
+ const limit = earlierForward ? e.y + e.height : e.y;
102
+ expect(l.y).toBeGreaterThanOrEqual(limit - 0.25 * EM - 0.01);
103
+ }
104
+ }
105
+ });
106
+ it('is deterministic', () => {
107
+ const again = letterPanel(panelFor(loc), itemsFor(loc));
108
+ expect(JSON.stringify(again)).toBe(JSON.stringify(out));
109
+ });
110
+ });
111
+ }
112
+ it('maps every printed character back to its source', () => {
113
+ const loc = LOCS[0];
114
+ const items = itemsFor(loc).map((it) => ({ ...it, sourceMap: Array.from({ length: it.text.length }, (_, k) => it.sourceStart + k) }));
115
+ const [b] = letterPanel(panelFor(loc), items);
116
+ const t = b.text[0];
117
+ expect(t.sourceText).toBe(t.lines.map((l) => l.text).join('\n'));
118
+ expect(t.sourceMap).toHaveLength(t.sourceText.length);
119
+ expect(t.sourceStart).toBe(0);
120
+ });
121
+ });
122
+ describe('letterPanel: kinds, joins, pins and fallbacks', () => {
123
+ const loc = LOCS[0];
124
+ const st = presetLetteringStyles({ fontSizePx: EM, locale: 'en', fontFamily: 'Test' });
125
+ it('butts a caption into the start corner (the right one when reading right to left)', () => {
126
+ const cap = { id: 'c', order: 0, kind: 'caption', text: 'Lyon, 1943.', sourceStart: 0, sourceEnd: 11, style: st.caption };
127
+ const [ltr] = letterPanel(panelFor(loc), [cap]);
128
+ expect(ltr.bbox.x).toBeCloseTo(0, 0);
129
+ expect(ltr.bbox.y).toBeCloseTo(0, 0);
130
+ expect(ltr.tailTip).toBeUndefined();
131
+ const [rtl] = letterPanel(panelFor(LOCS[3]), [{ ...cap, style: presetLetteringStyles({ fontSizePx: EM, locale: 'ar' }).caption }]);
132
+ expect(rtl.bbox.x + rtl.bbox.width).toBeCloseTo(600, 0);
133
+ });
134
+ it('joins two balloons of one speaker into one outline with one tail', () => {
135
+ const items = [
136
+ { id: 'a', order: 0, kind: 'balloon', speaker: 'ben', text: 'Listen to me.', sourceStart: 0, sourceEnd: 5, style: st.speech },
137
+ { id: 'b', order: 1, kind: 'balloon', speaker: 'ben', text: 'We leave tonight. Pack only what you can carry.', sourceStart: 10, sourceEnd: 20, style: st.speech },
138
+ ];
139
+ for (const join of ['butt', 'connector']) {
140
+ const out = letterPanel(panelFor(loc), [items[0], { ...items[1], join }]);
141
+ expect(out[0].group).toBe(out[1].group);
142
+ expect(out[0].shape).toBeDefined();
143
+ expect(out[1].shape).toBeUndefined();
144
+ expect(out[0].tailTip).toBeDefined();
145
+ expect(out[1].tailTip).toBeUndefined();
146
+ // Bodies, tail, (neck): several subpaths in one compound path.
147
+ expect((out[0].shape.d.match(/M/g) ?? []).length).toBeGreaterThanOrEqual(join === 'connector' ? 4 : 3);
148
+ }
149
+ // `join=false` keeps them apart.
150
+ const apart = letterPanel(panelFor(loc), [items[0], { ...items[1], join: false }]);
151
+ expect(apart[0].group).not.toBe(apart[1].group);
152
+ expect(apart[1].shape).toBeDefined();
153
+ });
154
+ it('honours a pinned centre', () => {
155
+ const pin = { x: 300, y: 80 };
156
+ const [b] = letterPanel(panelFor(loc), [{ id: 'p', order: 0, kind: 'balloon', speaker: 'ana', text: 'Here.', sourceStart: 0, sourceEnd: 5, style: st.speech, pin }]);
157
+ expect(b.bbox.x + b.bbox.width / 2).toBeCloseTo(pin.x, 0);
158
+ expect(b.bbox.y + b.bbox.height / 2).toBeCloseTo(pin.y, 0);
159
+ });
160
+ it('runs an off-panel tail to the border', () => {
161
+ const side = letterPanel(panelFor(loc), [{ id: 'o', order: 0, kind: 'balloon', speaker: 'zoe', text: 'Over here!', sourceStart: 0, sourceEnd: 5, style: st.speech, tailTarget: 'end' }]);
162
+ expect(side[0].tailTip.x).toBeCloseTo(600, 0);
163
+ const unknown = letterPanel(panelFor(loc), [{ id: 'o', order: 0, kind: 'balloon', speaker: 'zoe', text: 'Over here!', sourceStart: 0, sourceEnd: 5, style: st.speech }]);
164
+ const t = unknown[0].tailTip;
165
+ const onBorder = Math.min(t.x, 600 - t.x, t.y, 320 - t.y);
166
+ expect(onBorder).toBeLessThan(0.5);
167
+ // A cropped speaker: toward the anchor, stopping at the border.
168
+ const cropped = letterPanel(panelFor(loc, { anchors: [{ id: 'zoe', mouth: { x: 300, y: 500 }, visible: false }] }), [{ id: 'o', order: 0, kind: 'balloon', speaker: 'zoe', text: 'Down here!', sourceStart: 0, sourceEnd: 5, style: st.speech }]);
169
+ expect(cropped[0].tailTip.y).toBeCloseTo(320, 0);
170
+ });
171
+ it('sets a sound effect without a balloon, rotated, with a halo, near its anchor', () => {
172
+ const sfxAnchor = { id: 'sfx', mouth: { x: 300, y: 100 }, visible: true };
173
+ const [s] = letterPanel(panelFor(loc, { anchors: [ANA, BEN, sfxAnchor] }), [{ id: 's', order: 0, kind: 'sfx', text: 'Krak', sourceStart: 0, sourceEnd: 4, style: st.sfx, rotate: -10 }]);
174
+ expect(s.shape).toBeUndefined();
175
+ expect(s.rotate).toBe(-10);
176
+ expect(s.halo.width).toBeGreaterThan(0);
177
+ expect(s.text[0].lines[0].text).toBe('KRAK');
178
+ expect(pointInRect(sfxAnchor.mouth, s.bbox, 2 * EM)).toBe(true);
179
+ // Vertical in a vertical panel, unless the style says horizontal.
180
+ const ja = presetLetteringStyles({ fontSizePx: EM, locale: 'ja' }).sfx;
181
+ const [v] = letterPanel(panelFor(LOCS[2]), [{ id: 's', order: 0, kind: 'sfx', text: 'バキッ', sourceStart: 0, sourceEnd: 3, style: ja }]);
182
+ expect(v.text[0].vertical).toBeDefined();
183
+ const [h] = letterPanel(panelFor(LOCS[2]), [{ id: 's', order: 0, kind: 'sfx', text: 'バキッ', sourceStart: 0, sourceEnd: 3, style: { ...ja, writingMode: 'horizontal' } }]);
184
+ expect(h.text[0].vertical).toBeUndefined();
185
+ });
186
+ it('scales a sound effect without changing the dialogue size', () => {
187
+ const [a] = letterPanel(panelFor(loc), [{ id: 's', order: 0, kind: 'sfx', text: 'Krak', sourceStart: 0, sourceEnd: 4, style: st.sfx }]);
188
+ const [b] = letterPanel(panelFor(loc), [{ id: 's', order: 0, kind: 'sfx', text: 'Krak', sourceStart: 0, sourceEnd: 4, style: st.sfx, sizeScale: 2 }]);
189
+ expect(b.text[0].bbox.width).toBeCloseTo(2 * a.text[0].bbox.width, 0);
190
+ });
191
+ it('reports a balloon that cannot fit and still places it', () => {
192
+ const tiny = rect(0, 0, 110, 60);
193
+ const long = 'This is far too much text for a panel this small, and it will not fit anywhere at all.';
194
+ const res = letterPanelDetailed({ ...panelFor(loc), polygon: corners(tiny), bbox: tiny, anchors: [], avoid: [] }, [{ id: 'x', order: 0, kind: 'balloon', speaker: 'ana', text: long, sourceStart: 5, sourceEnd: 90, style: st.speech }]);
195
+ expect(res.balloons).toHaveLength(1);
196
+ expect(res.diagnostics).toHaveLength(1);
197
+ expect(res.diagnostics[0]).toMatchObject({ itemId: 'x', panelIndex: 0, sourceStart: 5, sourceEnd: 90 });
198
+ expect(res.diagnostics[0].reasons).toContain('outside');
199
+ // Never shrunk: the dialogue keeps its size.
200
+ expect(res.balloons[0].text[0].fontString).toContain(`${EM}px`);
201
+ });
202
+ it('breaks the border when the item allows it before reporting', () => {
203
+ const tiny = rect(0, 0, 110, 50);
204
+ const text = 'Just a bit too long for here.';
205
+ const base = { ...panelFor(loc), polygon: corners(tiny), bbox: tiny, anchors: [], avoid: [] };
206
+ const item = { id: 'x', order: 0, kind: 'balloon', text, sourceStart: 0, sourceEnd: 5, style: { ...st.speech, tail: 'none' } };
207
+ const strict = letterPanelDetailed(base, [item]);
208
+ const lenient = letterPanelDetailed(base, [{ ...item, breakBorder: true }]);
209
+ expect(strict.diagnostics).toHaveLength(1);
210
+ expect(strict.diagnostics[0].fallbacks).not.toContain('breakBorder');
211
+ expect(lenient.diagnostics).toHaveLength(0);
212
+ // It took the first fallback, keeping its preferred shape.
213
+ const b = lenient.balloons[0].bbox;
214
+ expect(b.x < 0 || b.y < 0 || b.x + b.width > 110 || b.y + b.height > 50).toBe(true);
215
+ });
216
+ it('covers an avoid zone (never a face) when nothing else fits', () => {
217
+ const cell = rect(0, 0, 200, 120);
218
+ const res = letterPanelDetailed({ ...panelFor(loc), polygon: corners(cell), bbox: cell, anchors: [], avoid: [rect(0, 0, 200, 120)] }, [{ id: 'x', order: 0, kind: 'balloon', text: 'Hello there.', sourceStart: 0, sourceEnd: 5, style: { ...st.speech, tail: 'none' } }]);
219
+ expect(res.diagnostics).toHaveLength(0);
220
+ const faces = letterPanelDetailed({ ...panelFor(loc), polygon: corners(cell), bbox: cell, anchors: [{ id: 'q', mouth: { x: -50, y: -50 }, face: rect(0, 0, 200, 120), visible: false }], avoid: [] }, [{ id: 'x', order: 0, kind: 'balloon', text: 'Hello there.', sourceStart: 0, sourceEnd: 5, style: { ...st.speech, tail: 'none' } }]);
221
+ expect(faces.diagnostics[0].reasons).toContain('face');
222
+ });
223
+ });
224
+ //# sourceMappingURL=letterPanel.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"letterPanel.test.js","sourceRoot":"","sources":["../../../../src/comics/lettering/__tests__/letterPanel.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=polish.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"polish.test.d.ts","sourceRoot":"","sources":["../../../../src/comics/lettering/__tests__/polish.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,357 @@
1
+ // The lettering rules added after looking at real pages (#559–#561): tails
2
+ // that never cross, heads kept clear, ruby and directional isolates inside
3
+ // balloons, house rules for CJK spacing and orphan lines, lettering that
4
+ // never runs into another panel or off the live area, pinned labels slid
5
+ // back into their panel, off-panel balloons by their border.
6
+ import { describe, it, expect, beforeAll } from 'vitest';
7
+ import { installStubMeasure } from './stubMeasure';
8
+ import { letterPanel, letterPanelDetailed } from '../letter';
9
+ import { presetLetteringStyles } from '../presets';
10
+ import { prepareText, readLetteringText, breakPoints } from '../text';
11
+ import { shapeText } from '../shape-text';
12
+ import { dist, segmentsCross } from '../geom';
13
+ import { buildBody } from '../shapes';
14
+ import { uncrossTails } from '../placement';
15
+ beforeAll(() => installStubMeasure());
16
+ const EM = 12;
17
+ const rect = (x, y, width, height) => ({ x, y, width, height });
18
+ const corners = (r) => [{ x: r.x, y: r.y }, { x: r.x + r.width, y: r.y }, { x: r.x + r.width, y: r.y + r.height }, { x: r.x, y: r.y + r.height }];
19
+ const overlap = (a, b) => Math.min(a.x + a.width, b.x + b.width) > Math.max(a.x, b.x) && Math.min(a.y + a.height, b.y + b.height) > Math.max(a.y, b.y);
20
+ const centre = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
21
+ const inside = (r, of, slack = 0.5) => r.x >= of.x - slack && r.y >= of.y - slack && r.x + r.width <= of.x + of.width + slack && r.y + r.height <= of.y + of.height + slack;
22
+ const en = presetLetteringStyles({ fontSizePx: EM, locale: 'en', fontFamily: 'Test' });
23
+ const ja = presetLetteringStyles({ fontSizePx: EM, locale: 'ja', fontFamily: 'Test' });
24
+ const ar = presetLetteringStyles({ fontSizePx: EM, locale: 'ar', fontFamily: 'Test' });
25
+ function panel(cell, extra = {}) {
26
+ return { index: 0, polygon: corners(cell), bbox: cell, insetPx: 6, direction: 'ltr', writingMode: 'horizontal', locale: 'en', anchors: [], dpi: 96, ...extra };
27
+ }
28
+ const item = (id, order, speaker, text, style = en.speech, extra = {}) => ({
29
+ id, order, kind: 'balloon', ...(speaker ? { speaker } : {}), text, sourceStart: order * 100, sourceEnd: order * 100 + 10, style, ...extra,
30
+ });
31
+ /** The drawn part of a tail: from where it leaves its balloon (on the
32
+ * line from the balloon's centre) to its tip. */
33
+ function tailOf(b) {
34
+ if (!b.tailTip)
35
+ return undefined;
36
+ const c = centre(b.bbox);
37
+ const t = b.tailTip;
38
+ const len = dist(c, t);
39
+ const k = Math.min(0.95, (Math.min(b.bbox.width, b.bbox.height) / 2) / Math.max(len, 1e-6));
40
+ return [{ x: c.x + (t.x - c.x) * k, y: c.y + (t.y - c.y) * k }, t];
41
+ }
42
+ /** A one-body unit of a `w`×`h` oval aimed at `target`. */
43
+ function unitOf(id, order, w, h, target) {
44
+ const body = buildBody([{ x: -w / 2 + 6, y: -h / 2 + 6, width: w - 12, height: h - 12 }], EM, en.speech, id);
45
+ const variant = { bodies: [body], reshaped: false, bbox: body.bbox, samples: [...body.core, body.centre], rim: body.hull };
46
+ return { id, order, kind: 'balloon', variants: [variant], target: { kind: 'point', point: target }, sourceStart: 0, sourceEnd: 1, em: EM };
47
+ }
48
+ describe('tails never cross', () => {
49
+ it('undoes a crossing by trading places (Kurlander routing, pairwise)', () => {
50
+ // Two balloons placed the wrong way round: each tail runs to the far
51
+ // speaker, across the other.
52
+ const CELL = rect(0, 0, 600, 360);
53
+ const p = panel(CELL);
54
+ const a = unitOf('a', 0, 120, 60, { x: 480, y: 300 });
55
+ const b = unitOf('b', 1, 120, 60, { x: 120, y: 300 });
56
+ const crossed = [{ unit: a, variant: 0, at: { x: 120, y: 120 } }, { unit: b, variant: 0, at: { x: 480, y: 120 } }];
57
+ const tail = (q) => {
58
+ const t = q.unit.target.point;
59
+ return [q.at, { x: q.at.x + (t.x - q.at.x) * 0.8, y: q.at.y + (t.y - q.at.y) * 0.8 }];
60
+ };
61
+ expect(segmentsCross(...tail(crossed[0]), ...tail(crossed[1]))).toBe(true);
62
+ const fixed = uncrossTails(p, crossed);
63
+ expect(segmentsCross(...tail(fixed[0]), ...tail(fixed[1]))).toBe(false);
64
+ // Each balloon now stands on its own speaker's side.
65
+ expect(fixed[0].at.x).toBeGreaterThan(fixed[1].at.x);
66
+ // A layout with no crossing is left alone.
67
+ expect(uncrossTails(p, fixed)).toEqual(fixed);
68
+ });
69
+ // Speakers stand in the order opposite to the one they speak in: the
70
+ // first speaker on the right, the second on the left, both low, so a
71
+ // reading-order layout (first balloon at the top left) would run its
72
+ // tail across the second one's.
73
+ const CELL = rect(0, 0, 640, 360);
74
+ const right = { id: 'r', mouth: { x: 520, y: 300 }, visible: true };
75
+ const left = { id: 'l', mouth: { x: 120, y: 300 }, visible: true };
76
+ for (const dir of ['ltr', 'rtl']) {
77
+ it(`swaps or re-places balloons whose tails would cross (${dir})`, () => {
78
+ const out = letterPanel(panel(CELL, { anchors: [right, left], direction: dir }), [
79
+ item('a', 0, dir === 'ltr' ? 'r' : 'l', 'You first. I insist, really.'),
80
+ item('b', 1, dir === 'ltr' ? 'l' : 'r', 'No, after you. I insist too.'),
81
+ ]);
82
+ const [ta, tb] = out.map(tailOf);
83
+ expect(ta && tb).toBeTruthy();
84
+ expect(segmentsCross(ta[0], ta[1], tb[0], tb[1])).toBe(false);
85
+ // Each tail still ends nearer its own speaker.
86
+ expect(dist(out[0].tailTip, (dir === 'ltr' ? right : left).mouth)).toBeLessThan(dist(out[0].tailTip, (dir === 'ltr' ? left : right).mouth));
87
+ });
88
+ }
89
+ it('keeps tails out of the other balloons', () => {
90
+ const CELL2 = rect(0, 0, 600, 400);
91
+ const a = { id: 'a', mouth: { x: 150, y: 330 }, visible: true };
92
+ const b = { id: 'b', mouth: { x: 450, y: 330 }, visible: true };
93
+ const out = letterPanel(panel(CELL2, { anchors: [a, b] }), [
94
+ item('1', 0, 'a', 'Did you see where the boat went?'),
95
+ item('2', 1, 'b', 'Out past the lighthouse.'),
96
+ item('3', 2, 'a', 'In this weather?'),
97
+ ]);
98
+ for (const x of out) {
99
+ const t = tailOf(x);
100
+ if (!t)
101
+ continue;
102
+ for (const y of out) {
103
+ if (y === x || y.group === x.group)
104
+ continue;
105
+ const box = { x: y.bbox.x + 0.15 * y.bbox.width, y: y.bbox.y + 0.15 * y.bbox.height, width: 0.7 * y.bbox.width, height: 0.7 * y.bbox.height };
106
+ const steps = 20;
107
+ for (let k = 0; k <= steps; k++) {
108
+ const p = { x: t[0].x + ((t[1].x - t[0].x) * k) / steps, y: t[0].y + ((t[1].y - t[0].y) * k) / steps };
109
+ expect(p.x > box.x && p.x < box.x + box.width && p.y > box.y && p.y < box.y + box.height).toBe(false);
110
+ }
111
+ }
112
+ }
113
+ });
114
+ });
115
+ describe('three speakers in a row', () => {
116
+ // Faces high, little sky: the script order (right, middle, left) is the
117
+ // reverse of where they stand; no two tails may cross.
118
+ const CELL = rect(0, 0, 900, 420);
119
+ const faces = [rect(90, 60, 110, 110), rect(390, 40, 110, 110), rect(690, 50, 110, 110)];
120
+ const anchors = ['lola', 'paco', 'carmen'].map((id, i) => ({ id, mouth: { x: faces[i].x + 55, y: faces[i].y + 85 }, face: faces[i], visible: true }));
121
+ const orders = [['carmen', 'paco', 'lola'], ['lola', 'carmen', 'paco'], ['paco', 'carmen', 'lola']];
122
+ for (const order of orders) {
123
+ it(`never crosses tails (${order.join(' → ')})`, () => {
124
+ const lines = { carmen: 'Come on, with three of us this will take no time.', paco: 'This beret has never carried anything so good.', lola: 'Sorry, Carmen!' };
125
+ const out = letterPanel(panel(CELL, { anchors }), order.map((sp, k) => item(String(k), k, sp, lines[sp])));
126
+ const tails = out.map(tailOf);
127
+ for (let i = 0; i < tails.length; i++) {
128
+ for (let j = i + 1; j < tails.length; j++)
129
+ expect(segmentsCross(tails[i][0], tails[i][1], tails[j][0], tails[j][1])).toBe(false);
130
+ }
131
+ // (The outline may graze a face's box; the text never covers one.)
132
+ const inner = (r) => ({ x: r.x + 0.08 * r.width, y: r.y + 0.08 * r.height, width: 0.84 * r.width, height: 0.84 * r.height });
133
+ for (const b of out)
134
+ for (const f of faces)
135
+ expect(overlap(inner(b.bbox), f)).toBe(false);
136
+ });
137
+ }
138
+ });
139
+ describe('corner captions and joined pairs', () => {
140
+ it('moves a corner caption to the other corner when its own holds a face', () => {
141
+ const CELL = rect(0, 0, 500, 300);
142
+ const face = rect(380, 0, 110, 120);
143
+ const [c] = letterPanel(panel(CELL, { direction: 'rtl', locale: 'ar', anchors: [{ id: 'carmen', mouth: { x: 435, y: 100 }, face, visible: true }] }), [
144
+ { id: 'c', order: 0, kind: 'caption', text: 'In the market.', sourceStart: 0, sourceEnd: 5, style: ar.caption },
145
+ ]);
146
+ expect(overlap(c.bbox, face)).toBe(false);
147
+ // The top-end corner of a right-to-left page: the left one.
148
+ expect(c.bbox.x).toBeLessThan(10);
149
+ expect(c.bbox.y).toBeLessThan(10);
150
+ });
151
+ it('never sets a joined balloon back against the reading direction', () => {
152
+ const CELL = rect(0, 0, 260, 520);
153
+ const k = { id: 'tomas', mouth: { x: 150, y: 470 }, visible: true };
154
+ const out = letterPanel(panel(CELL, { anchors: [k] }), [
155
+ item('a', 0, 'tomas', 'Just in time. The radio is gone deaf,'),
156
+ item('b', 1, 'tomas', 'and there is a storm on the way.'),
157
+ ]);
158
+ expect(out[0].group).toBe(out[1].group);
159
+ // The second reads after the first: below it, not up and to its left.
160
+ const a = out[0].bbox;
161
+ const b = out[1].bbox;
162
+ expect(b.y + b.height / 2).toBeGreaterThan(a.y + a.height / 2);
163
+ expect(b.x + b.width / 2).toBeGreaterThanOrEqual(a.x + a.width / 2 - 0.5 * a.width);
164
+ });
165
+ });
166
+ describe('heads and hats kept clear', () => {
167
+ it('keeps balloons off the region a guard marks above a face (hat, hair)', () => {
168
+ // The layout passes a face grown upward as an avoid zone (see
169
+ // `headGuards`); with room elsewhere, no balloon covers it.
170
+ const CELL = rect(0, 0, 600, 360);
171
+ const face = rect(250, 150, 100, 100);
172
+ const hat = rect(230, 110, 140, 40);
173
+ const out = letterPanel(panel(CELL, { anchors: [{ id: 'a', mouth: { x: 300, y: 225 }, face, visible: true }], avoid: [{ x: 230, y: 110, width: 140, height: 140 }] }), [
174
+ item('1', 0, 'a', 'This hat has seen three storms and a wedding.'),
175
+ ]);
176
+ expect(overlap(out[0].bbox, hat)).toBe(false);
177
+ expect(overlap(out[0].bbox, face)).toBe(false);
178
+ });
179
+ });
180
+ describe('ruby and isolates inside balloons', () => {
181
+ const rubySpans = (vertical) => [
182
+ { text: '灯台', bold: false, italic: false, ruby: { text: 'とうだい', group: true, id: 1 } },
183
+ { text: 'の', bold: false, italic: false },
184
+ { text: '嵐', bold: false, italic: false, ruby: { text: 'あらし', group: true, id: 2 } },
185
+ { text: vertical ? 'が来る' : 'が来る', bold: false, italic: false },
186
+ ];
187
+ it('sets the reading over its base in horizontal lines', () => {
188
+ const p = panel(rect(0, 0, 600, 360), { locale: 'ja' });
189
+ const [b] = letterPanel(p, [item('1', 0, undefined, rubySpans(false), { ...ja.speech, writingMode: 'horizontal' })]);
190
+ const [base, ...rubies] = b.text;
191
+ expect(base.lines.map((l) => l.text).join('')).toBe('灯台の嵐が来る');
192
+ expect(rubies.map((r) => r.lines[0].text)).toEqual(['とうだい', 'あらし']);
193
+ for (const r of rubies) {
194
+ // Half the size, above the line, never read twice by assistive tech.
195
+ expect(r.fontString).toContain(`${EM / 2}px`);
196
+ const bottom = r.bbox.y + r.bbox.height;
197
+ expect(base.lines.some((l) => bottom <= l.baselineY - 0.5 * EM && bottom > l.baselineY - 1.3 * EM)).toBe(true);
198
+ expect(r.artifact).toBe(true);
199
+ }
200
+ // Centred over its base (灯台 starts the only line).
201
+ const line = base.lines[0];
202
+ const baseCentre = base.bbox.x + line.xOffset + EM; // two ems of 灯台
203
+ expect(Math.abs(centre(rubies[0].bbox).x - baseCentre)).toBeLessThan(0.6 * EM);
204
+ // The balloon holds the reading too.
205
+ for (const r of rubies)
206
+ expect(inside(r.bbox, b.bbox, 1)).toBe(true);
207
+ });
208
+ it('sets the reading beside its base in columns', () => {
209
+ const p = panel(rect(0, 0, 400, 500), { locale: 'ja', writingMode: 'vertical', direction: 'rtl' });
210
+ const [b] = letterPanel(p, [item('1', 0, undefined, rubySpans(true), ja.speech)]);
211
+ const [base, ...rubies] = b.text;
212
+ expect(base.vertical).toBeDefined();
213
+ expect(rubies).toHaveLength(2);
214
+ for (const r of rubies) {
215
+ expect(r.vertical).toBeDefined();
216
+ // Right of a column: past the column's centre line by half an em.
217
+ expect(r.bbox.width).toBeLessThan(EM);
218
+ }
219
+ // とうだい lies right of 灯台's column, centred on it along the column.
220
+ const first = rubies[0];
221
+ expect(first.bbox.x).toBeGreaterThan(base.bbox.x + base.bbox.width - 1.2 * EM);
222
+ expect(inside(first.bbox, b.bbox, 1)).toBe(true);
223
+ });
224
+ it('keeps a :ltr[…] run whole inside Arabic, printing no control', () => {
225
+ const spans = [
226
+ { text: 'وصلتُ على متن ', bold: false, italic: false },
227
+ { text: 'Bus 42', bold: false, italic: false, direction: { dir: 'ltr', id: 1 } },
228
+ { text: '!', bold: false, italic: false },
229
+ ];
230
+ const read = readLetteringText(spans, Array.from({ length: 21 }, (_, k) => k));
231
+ expect(read.text).toBe('وصلتُ على متن ⁦Bus 42⁩!');
232
+ const p = panel(rect(0, 0, 600, 360), { locale: 'ar', direction: 'rtl' });
233
+ const [b] = letterPanel(p, [item('1', 0, undefined, spans, ar.speech, { sourceMap: Array.from({ length: 21 }, (_, k) => k) })]);
234
+ const t = b.text[0];
235
+ expect(t.direction).toBe('rtl');
236
+ for (const l of t.lines) {
237
+ expect(/[⁦-⁩]/.test(l.text)).toBe(false);
238
+ for (const r of l.runs ?? [])
239
+ expect(/[⁦-⁩]/.test(r.text)).toBe(false);
240
+ }
241
+ // The isolate is one left-to-right run, not reordered word by word.
242
+ const runs = t.lines.flatMap((l) => l.runs ?? []);
243
+ expect(runs.some((r) => !r.rtl && r.text.includes('Bus 42'))).toBe(true);
244
+ // Click to source still maps every printed character.
245
+ expect(t.sourceMap).toBeDefined();
246
+ expect(t.sourceMap.every((m) => m >= 0)).toBe(true);
247
+ });
248
+ it('nests isolates and closes them at the end', () => {
249
+ const outer = { dir: 'rtl', id: 1 };
250
+ const spans = [
251
+ { text: 'a ', bold: false, italic: false },
252
+ { text: 'b ', bold: false, italic: false, direction: outer },
253
+ { text: 'c', bold: false, italic: false, direction: { dir: 'ltr', id: 2, outer } },
254
+ ];
255
+ expect(readLetteringText(spans, undefined).text).toBe('a ⁧b ⁦c⁩⁩');
256
+ });
257
+ });
258
+ describe('house rules for CJK and Latin lines', () => {
259
+ const prep = (text, locale, vertical = false) => prepareText(readLetteringText(text, undefined), (locale === 'ja' ? ja : en).speech, { locale, vertical });
260
+ it('drops the spaces a translator left between CJK characters and marks', () => {
261
+ expect(prep('我らに何の破壊を願う ? …', 'ja').text).toBe('我らに何の破壊を願う?…');
262
+ expect(prep('… さあできた', 'ja').text).toBe('…さあできた');
263
+ // Latin words keep theirs.
264
+ expect(prep('エピソード 8 : Pepper', 'ja').text).toBe('エピソード 8 : Pepper');
265
+ });
266
+ it('never breaks before a closing mark or after an opening one', () => {
267
+ const p = prep('Papi ! Je suis là ! « Bonjour »', 'fr');
268
+ const ends = breakPoints(p, 'gb', false).filter((b) => !b.forced).map((b) => p.text.slice(b.next, b.next + 1));
269
+ expect(ends).not.toContain('!');
270
+ expect(ends).not.toContain('»');
271
+ const starts = breakPoints(p, 'gb', false).filter((b) => !b.forced).map((b) => p.text[b.end - 1]);
272
+ expect(starts).not.toContain('«');
273
+ });
274
+ it('never leaves a leader or a single character alone on a column', () => {
275
+ const p = prep('……みんなが頼りにならないなら……', 'ja', true);
276
+ const s = shapeText(p, ja.speech, { locale: 'ja', vertical: true, dpi: 96 });
277
+ for (const l of s.lines) {
278
+ const t = p.text.slice(l.start, l.end).replace(/[\s\p{P}\p{S}]/gu, '');
279
+ expect([...t].length).toBeGreaterThan(1);
280
+ }
281
+ });
282
+ it('breaks a short Japanese text inside no word (わざ|わざ)', () => {
283
+ const p = prep('……わざわざ?', 'ja', true);
284
+ const s = shapeText(p, ja.speech, { locale: 'ja', vertical: true, dpi: 96 });
285
+ const lines = s.lines.map((l) => p.text.slice(l.start, l.end));
286
+ expect(lines.some((t) => t.endsWith('わざ') && !t.endsWith('わざわざ'))).toBe(false);
287
+ });
288
+ it('sets a text with no CJK horizontally in a vertical panel (an English subtitle)', () => {
289
+ const p = panel(rect(0, 0, 400, 400), { locale: 'ja', writingMode: 'vertical', direction: 'rtl' });
290
+ const [s, b] = letterPanel(p, [
291
+ { id: 's', order: 0, kind: 'sfx', text: 'WHAM', sourceStart: 0, sourceEnd: 4, style: ja.sfx },
292
+ item('b', 1, undefined, 'ドン', ja.speech),
293
+ ]);
294
+ expect(s.text[0].vertical).toBeUndefined();
295
+ expect(b.text[0].vertical).toBeDefined();
296
+ });
297
+ });
298
+ describe('lettering stays where it belongs', () => {
299
+ it('slides a pinned label that would run out of its panel back inside', () => {
300
+ const CELL = rect(0, 0, 300, 200);
301
+ const pin = { x: 290, y: 190 };
302
+ const [b] = letterPanel(panel(CELL), [{ id: 'w', order: 0, kind: 'caption', text: 'Incantations for Demons, Vol. 1', sourceStart: 0, sourceEnd: 5, style: { ...en.caption, position: 'auto' }, pin }]);
303
+ expect(inside(b.bbox, CELL)).toBe(true);
304
+ // A pin with room is honoured exactly.
305
+ const [c] = letterPanel(panel(CELL), [{ id: 'w', order: 0, kind: 'caption', text: 'Vol. 1', sourceStart: 0, sourceEnd: 5, style: { ...en.caption, position: 'auto' }, pin: { x: 150, y: 100 } }]);
306
+ expect(centre(c.bbox).x).toBeCloseTo(150, 0);
307
+ expect(centre(c.bbox).y).toBeCloseTo(100, 0);
308
+ });
309
+ it('never runs into another panel or off the live area, even overflowing', () => {
310
+ // A tiny panel with a long shout; the panel above holds a balloon.
311
+ const CELL = rect(0, 210, 260, 150);
312
+ const above = rect(0, 0, 260, 200);
313
+ const limit = rect(0, 0, 260, 360);
314
+ const { balloons, diagnostics } = letterPanelDetailed(panel(CELL, { neighbours: [above], limit, anchors: [{ id: 'k', mouth: { x: 60, y: 300 }, visible: true }] }), [
315
+ item('1', 0, 'k', "Can't work at all and I've never been happier in my whole life!!", en.shout),
316
+ ]);
317
+ const b = balloons[0];
318
+ expect(overlap(b.bbox, { x: above.x, y: above.y, width: above.width, height: above.height - 2 })).toBe(false);
319
+ expect(b.bbox.x).toBeGreaterThanOrEqual(limit.x - 0.5);
320
+ expect(b.bbox.x + b.bbox.width).toBeLessThanOrEqual(limit.x + limit.width + 0.5);
321
+ // It does not fit cleanly, and says so; the size never changes.
322
+ expect(diagnostics.length).toBe(1);
323
+ expect(b.text[0].fontString).toContain(`${en.shout.fontSizePx}px`);
324
+ });
325
+ it('puts an off-panel speaker\'s balloon by the border its tail runs to', () => {
326
+ const CELL = rect(0, 0, 600, 400);
327
+ const [b] = letterPanel(panel(CELL), [item('1', 0, 'zoe', 'Thank you, keeper!', en.speech, { tailTarget: 'bottom' })]);
328
+ expect(b.tailTip.y).toBeCloseTo(400, 0);
329
+ // The tail is short: the balloon sits low, by the bottom border.
330
+ expect(dist(centre(b.bbox), b.tailTip)).toBeLessThan(b.bbox.height / 2 + 3 * EM);
331
+ });
332
+ it('butts a caption against the inner edge of a framed panel\'s border', () => {
333
+ const CELL = rect(0, 0, 400, 300);
334
+ const [c] = letterPanel(panel(CELL, { borderPx: 4 }), [{ id: 'c', order: 0, kind: 'caption', text: 'Lyon, 1943.', sourceStart: 0, sourceEnd: 5, style: en.caption, position: 'top-start' }]);
335
+ expect(c.bbox.x).toBeCloseTo(2, 1);
336
+ expect(c.bbox.y).toBeCloseTo(2, 1);
337
+ });
338
+ it('places the dialogue before a sound effect with no place of its own', () => {
339
+ const CELL = rect(0, 0, 400, 260);
340
+ const k = { id: 'k', mouth: { x: 100, y: 200 }, visible: true };
341
+ const out = letterPanel(panel(CELL, { anchors: [k] }), [
342
+ { id: 's', order: 0, kind: 'sfx', text: 'THUMP', sourceStart: 0, sourceEnd: 5, style: en.sfx },
343
+ item('b', 1, 'k', 'Ouch.'),
344
+ ]);
345
+ const balloon = out.find((b) => b.kind === 'balloon');
346
+ // The balloon takes its spot near the speaker, above it.
347
+ expect(balloon.bbox.y + balloon.bbox.height).toBeLessThanOrEqual(k.mouth.y);
348
+ expect(dist(centre(balloon.bbox), k.mouth)).toBeLessThan(8 * EM);
349
+ });
350
+ it('is deterministic', () => {
351
+ const CELL = rect(0, 0, 640, 360);
352
+ const anchors = [{ id: 'r', mouth: { x: 520, y: 300 }, visible: true }, { id: 'l', mouth: { x: 120, y: 300 }, visible: true }];
353
+ const run = () => JSON.stringify(letterPanel(panel(CELL, { anchors }), [item('a', 0, 'r', 'You first.'), item('b', 1, 'l', 'No, you.')]));
354
+ expect(run()).toBe(run());
355
+ });
356
+ });
357
+ //# sourceMappingURL=polish.test.js.map