@unmade/text-renderer 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -0
- package/dist/{__vite-browser-external-CijFC1_R.js → __vite-browser-external-oLchLuMx.js} +1 -1
- package/dist/baselines/index.d.ts +19 -0
- package/dist/ce-adapter/index.cjs +1 -1
- package/dist/ce-adapter/index.js +1 -1
- package/dist/fitting/index.d.ts +86 -0
- package/dist/helpers/bbox.d.ts +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1143 -339
- package/dist/layout/index.d.ts +77 -0
- package/dist/node/baselines/index.d.ts +19 -0
- package/dist/node/fitting/index.d.ts +86 -0
- package/dist/node/helpers/bbox.d.ts +2 -1
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +446 -201
- package/dist/node/layout/index.d.ts +77 -0
- package/dist/node/renderText.d.ts +3 -0
- package/dist/node/rendering/index.d.ts +2 -2
- package/dist/node/types.d.ts +3 -0
- package/dist/renderText.d.ts +3 -0
- package/dist/rendering/index.d.ts +2 -2
- package/dist/{chunk-3b4jIN3o.js → rolldown-runtime-DtPi1Y-2.js} +2 -2
- package/dist/types.d.ts +3 -0
- package/dist/worker/baselines/index.d.ts +19 -0
- package/dist/worker/fitting/index.d.ts +86 -0
- package/dist/worker/helpers/bbox.d.ts +2 -1
- package/dist/worker/index.d.ts +2 -0
- package/dist/worker/index.js +2846 -2377
- package/dist/worker/layout/index.d.ts +77 -0
- package/dist/worker/renderText.d.ts +3 -0
- package/dist/worker/rendering/index.d.ts +2 -2
- package/dist/worker/types.d.ts +3 -0
- package/package.json +12 -5
- package/dist/helpers/matrix.d.ts +0 -8
- package/dist/node/helpers/matrix.d.ts +0 -8
- package/dist/worker/helpers/matrix.d.ts +0 -8
package/README.md
CHANGED
|
@@ -46,6 +46,154 @@ const { getRenderedText } = await import('@unmade/text-renderer/node');
|
|
|
46
46
|
|
|
47
47
|
**CE Lambda integration**: When wiring text-renderer into the Configuration Engine Lambda, use the `/node` ESM entry (`@unmade/text-renderer/node`) from an ESM Lambda handler, or use the `await import()` pattern above from an existing CJS handler. The `ce-adapter` sub-package (`@unmade/text-renderer/ce-adapter`) retains a CJS build and can be `require()`'d normally.
|
|
48
48
|
|
|
49
|
+
## Choosing a font size
|
|
50
|
+
|
|
51
|
+
`getRenderedText` draws text at a size you give it. These work out what that size should be.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { getFontSizeToFitBox, loadFont } from '@unmade/text-renderer';
|
|
55
|
+
|
|
56
|
+
const font = await loadFont('https://example.com/font.ttf');
|
|
57
|
+
|
|
58
|
+
const { fontSize, fits, boundBy } = getFontSizeToFitBox({
|
|
59
|
+
font,
|
|
60
|
+
text: 'HELLO',
|
|
61
|
+
boxDimensions: {
|
|
62
|
+
width: 500,
|
|
63
|
+
height: 200,
|
|
64
|
+
physicalWidth: 100,
|
|
65
|
+
physicalHeight: 40,
|
|
66
|
+
physicalUnits: 'mm',
|
|
67
|
+
},
|
|
68
|
+
size: { mode: 'fitToBox', minFontSize: 20, maxFontSize: 200 },
|
|
69
|
+
spacing: { outlineWidth: 0, letterSpacing: 0, letterSpacingOutline: 0 },
|
|
70
|
+
baseline: 'flat', // or 'curved', or { type: 'custom', path: '...' }
|
|
71
|
+
verticalAlignment: 'centre', // affects where a curved baseline sits
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For flat and custom baselines this is a direct calculation rather than a search: glyph
|
|
76
|
+
widths, outline stroke and letter spacing are all `fontSize * constant`, so measuring
|
|
77
|
+
once at a reference size and solving for a scale factor is exact. Curved baselines are
|
|
78
|
+
bisected instead, against both limits at once: neither the arc's length nor the arch's
|
|
79
|
+
position is proportional to font size, so both are evaluated at each candidate size.
|
|
80
|
+
|
|
81
|
+
The height check asks where the text will actually land, not how tall it is. The arch is
|
|
82
|
+
positioned from a standard capital, so that its shape does not change as characters are
|
|
83
|
+
typed — which means a descender hangs below the arch's ends, outside a box the arch itself
|
|
84
|
+
sits comfortably inside. The check reads the arch's placement from the same function that
|
|
85
|
+
generates the baseline path, so a size it accepts is one the renderer can draw.
|
|
86
|
+
|
|
87
|
+
### The three ways to decide a size
|
|
88
|
+
|
|
89
|
+
`size` covers each of them, so none is a special case:
|
|
90
|
+
|
|
91
|
+
| `size` | Meaning |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `{ mode: 'fitToBox', minFontSize, maxFontSize }` | Solve for the largest size that fits the box |
|
|
94
|
+
| `{ mode: 'fontSize', fontSize }` | Pin a size in pixels |
|
|
95
|
+
| `{ mode: 'physicalSize', physicalSize: [60, 'mm'] }` | Pin a physical size — what a fixed size preset is |
|
|
96
|
+
|
|
97
|
+
`minFontSize` and `maxFontSize` belong only to `fitToBox`, because they only mean
|
|
98
|
+
anything while solving. A pinned size overrides them by design.
|
|
99
|
+
|
|
100
|
+
Pinning inverts the problem. Instead of the text being fixed and the size free, the
|
|
101
|
+
size is fixed and the **text length** becomes the free variable — hold 60mm, and fit as
|
|
102
|
+
many characters as will go.
|
|
103
|
+
|
|
104
|
+
### Reading the result
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
interface FitTextToBoxResult {
|
|
108
|
+
fontSize: number;
|
|
109
|
+
fits: boolean; // false when it won't fit even at the smallest allowed size
|
|
110
|
+
boundBy: 'width' | 'height' | 'requestedSize';
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`boundBy` says which limit decided the outcome — when it fits, what capped the size;
|
|
115
|
+
when it doesn't, what couldn't be satisfied. Callers generally need to respond
|
|
116
|
+
differently to each: text too wide for its box and text too tall for its box call for
|
|
117
|
+
different remedies, and in the configuration engine they surface as different messages.
|
|
118
|
+
|
|
119
|
+
`requestedSize` means the box wasn't the limiting factor: either `maxFontSize` was
|
|
120
|
+
reached while solving, or a pinned size was simply granted.
|
|
121
|
+
|
|
122
|
+
### Shortening text that won't fit
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { fitAndTruncateTextToBox } from '@unmade/text-renderer';
|
|
126
|
+
|
|
127
|
+
const result = fitAndTruncateTextToBox({ font, text, boxDimensions, size, spacing });
|
|
128
|
+
|
|
129
|
+
result.text; // possibly shortened
|
|
130
|
+
result.truncated; // whether anything was removed
|
|
131
|
+
result.linesDropped; // whole lines removed
|
|
132
|
+
result.charactersRemoved; // trailing characters removed
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Whole lines are dropped before characters are trimmed. Both counts are reported rather
|
|
136
|
+
than a single mode, because one call can do both — drop a line, then find the remainder
|
|
137
|
+
still needs trimming — and because the caller usually wants to say something different
|
|
138
|
+
about each.
|
|
139
|
+
|
|
140
|
+
Works in every `size` mode. With a pinned size it is the only thing that can give: the
|
|
141
|
+
size stays put and the text shortens.
|
|
142
|
+
|
|
143
|
+
### Rotation
|
|
144
|
+
|
|
145
|
+
Deliberately absent. Pass the box you want text laid out in and apply rotation to the
|
|
146
|
+
rendered output. Rotating the frame rather than reasoning about rotation inside the
|
|
147
|
+
solver keeps it working in one coordinate space, and is also what lets a rotated tall
|
|
148
|
+
placement use its long side for the baseline.
|
|
149
|
+
|
|
150
|
+
## Measuring text
|
|
151
|
+
|
|
152
|
+
`renderText` returns artwork; `measureText` returns its dimensions, for the same
|
|
153
|
+
inputs.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { measureText } from '@unmade/text-renderer';
|
|
157
|
+
|
|
158
|
+
const m = measureText({
|
|
159
|
+
font,
|
|
160
|
+
text: 'HELLO',
|
|
161
|
+
boxDimensions,
|
|
162
|
+
physicalSize: [20, 'mm'],
|
|
163
|
+
spacing: { outlineWidth: 0, letterSpacing: 0, letterSpacingOutline: 0 },
|
|
164
|
+
baseline: 'flat',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
m.bbox; // { x, y, width, height } in box pixels
|
|
168
|
+
m.physicalWidth; // in the box's units
|
|
169
|
+
m.physicalHeight;
|
|
170
|
+
m.physicalCapHeight; // see below
|
|
171
|
+
m.lines; // per line, in rendered order
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
It measures the **same positioned glyphs that get drawn**, rather than
|
|
175
|
+
re-deriving dimensions from font metrics. That matters for more than tidiness: a
|
|
176
|
+
curved line's bounds follow the arch, which no flat metrics calculation can know
|
|
177
|
+
about, and any second implementation eventually disagrees with the first.
|
|
178
|
+
`renderText` and `measureText` share one layout pass for exactly that reason.
|
|
179
|
+
|
|
180
|
+
It throws where `renderText` would throw, and for the same reasons — so if
|
|
181
|
+
measuring succeeds, rendering the same options will too.
|
|
182
|
+
|
|
183
|
+
### Cap height is reported, not derived
|
|
184
|
+
|
|
185
|
+
`physicalCapHeight` is the physical height of a standard capital at the chosen
|
|
186
|
+
font size. It is **not** `physicalHeight`, and can't be calculated from it:
|
|
187
|
+
|
|
188
|
+
- `physicalHeight` measures the glyphs actually present, so it grows with
|
|
189
|
+
descenders, accents and punctuation
|
|
190
|
+
- `physicalCapHeight` is a property of the font and size alone, so the same size
|
|
191
|
+
always reports the same cap height regardless of what was typed
|
|
192
|
+
|
|
193
|
+
Both are needed. Cap height is what a fixed size preset targets and what gets
|
|
194
|
+
persisted against a design for manufacturing; the measured height is what the
|
|
195
|
+
artwork actually occupies.
|
|
196
|
+
|
|
49
197
|
## Configuration Engine adapter
|
|
50
198
|
|
|
51
199
|
The `ce-adapter` sub-package bridges CE (Configuration Engine) editor state to renderer options:
|
|
@@ -88,6 +236,89 @@ comparisons/{designUrlHash}/{timestamp}/{placementId}/
|
|
|
88
236
|
renderer-options.json # GetRenderedTextOptions (mapped TR input)
|
|
89
237
|
```
|
|
90
238
|
|
|
239
|
+
## Platform bundle suites
|
|
240
|
+
|
|
241
|
+
The package builds a separate bundle per platform — browser, worker and node —
|
|
242
|
+
each resolving a different variant of `@unmade/platform`:
|
|
243
|
+
|
|
244
|
+
| | `DOMPoint` | `DOMMatrix` | `DOMParser` |
|
|
245
|
+
|---|---|---|---|
|
|
246
|
+
| node | polyfill | `@thednp` polyfill | linkedom |
|
|
247
|
+
| worker | **native** | `@thednp` polyfill | linkedom/worker |
|
|
248
|
+
| browser | native | native | native |
|
|
249
|
+
|
|
250
|
+
`__tests__/platform/` tests those built bundles, each in the host it ships into:
|
|
251
|
+
the node bundle in the test process, the browser bundle on a page, and the worker
|
|
252
|
+
bundle inside a real `DedicatedWorkerGlobalScope`. Nothing else runs them — the
|
|
253
|
+
unit suite tests `src` under node, where neither `DOMPoint` nor `DOMMatrix`
|
|
254
|
+
exists, so it can only ever exercise the polyfills.
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
npm run test:platform
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
It builds first, because it loads `dist` and a stale bundle would otherwise give
|
|
261
|
+
a green run against code that is no longer there. It needs a browser:
|
|
262
|
+
`npx playwright install chromium`.
|
|
263
|
+
|
|
264
|
+
### The two suites
|
|
265
|
+
|
|
266
|
+
**`render.test.ts`** draws every scenario in `scenarios.ts` on all three bundles,
|
|
267
|
+
through both public render entry points — `getRenderedText`, which loads its own
|
|
268
|
+
font, and `renderText`, which takes one already loaded. Each render is rasterised
|
|
269
|
+
here, by one resvg call for all three, and checked twice:
|
|
270
|
+
|
|
271
|
+
- **pixel-identical to the node bundle's, with no tolerance.** All three go
|
|
272
|
+
through the same rasteriser in the same process, so nothing legitimate can move
|
|
273
|
+
a pixel. This is the assertion that the differing geometry and DOM
|
|
274
|
+
implementations do not reach the artwork.
|
|
275
|
+
- **within 1% of a committed reference** in `platform/references/`, so a change
|
|
276
|
+
that shifts all three together is caught as well.
|
|
277
|
+
|
|
278
|
+
**`api.test.ts`** covers the rest of the public API, which nothing else would
|
|
279
|
+
call on the browser or worker bundles: constants, baseline generation and
|
|
280
|
+
validation, measurement, font loading and the font cache. Each is called with
|
|
281
|
+
identical inputs on all three bundles and the results compared. It also asserts
|
|
282
|
+
the three export the same names, that every export has a probe, and that each
|
|
283
|
+
bundle ran in the host it is built for — a `typeof DOMPoint` check cannot tell a
|
|
284
|
+
page from a worker, since both have the native globals.
|
|
285
|
+
|
|
286
|
+
### The report
|
|
287
|
+
|
|
288
|
+
Every run writes `__tests__/platform-output/report.html`: one self-contained file
|
|
289
|
+
with each scenario's committed reference beside what each bundle drew, badged
|
|
290
|
+
with both comparisons, plus the diff images and the browser's user agent. It is
|
|
291
|
+
gitignored and collected as a CI artifact from `platform-test` whether the run
|
|
292
|
+
passed or failed, so a by-eye check is always available. The SVG each bundle
|
|
293
|
+
produced is written alongside it, so a difference the report shows as pixels can
|
|
294
|
+
be diffed as text.
|
|
295
|
+
|
|
296
|
+
### Fonts
|
|
297
|
+
|
|
298
|
+
Served from the test process, so every platform fetches byte-identical bytes from
|
|
299
|
+
the same URL and the font is never a variable. Three, chosen for what they
|
|
300
|
+
exercise:
|
|
301
|
+
|
|
302
|
+
- **Bangers** — irregular display face: curves, diagonals, varying stroke widths.
|
|
303
|
+
Caps only.
|
|
304
|
+
- **Lora** — high-contrast serif with true lowercase, so descenders feed the
|
|
305
|
+
baseline and line-height calculation.
|
|
306
|
+
- **giants-color** — glyphs stored as SVG documents, the only path that goes
|
|
307
|
+
through a `DOMParser` and back out through an `XMLSerializer`.
|
|
308
|
+
|
|
309
|
+
The two added faces are OFL, with their licences beside them in
|
|
310
|
+
`__tests__/fixtures/`.
|
|
311
|
+
|
|
312
|
+
### Updating the references
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
UPDATE_FIXTURES=true npm run test:platform
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Rewrites every reference from the node bundle's render, then still checks the
|
|
319
|
+
other two against what it just stored. Review the diffs in the report before
|
|
320
|
+
committing.
|
|
321
|
+
|
|
91
322
|
## Regression tests
|
|
92
323
|
|
|
93
324
|
Regression tests live in `__tests__/regression.test.ts` and use fixture pairs in `__tests__/fixtures/regression/`:
|
|
@@ -28,6 +28,25 @@ export interface GenerateCurvedBaselineOptions {
|
|
|
28
28
|
textHeight: number;
|
|
29
29
|
verticalAlignment: VerticalTextAlignmentValues;
|
|
30
30
|
}
|
|
31
|
+
/** Where the arch sits vertically: its peak, and the baseline at its ends. */
|
|
32
|
+
export interface CurvedBaselineGeometry {
|
|
33
|
+
/** y of the peak — the highest point of the baseline. */
|
|
34
|
+
curveTop: number;
|
|
35
|
+
/** y of the two ends — the lowest points of the baseline. */
|
|
36
|
+
curveBottom: number;
|
|
37
|
+
/** Horizontal gap left at each end so the outer glyphs are not clipped. */
|
|
38
|
+
curveXInset: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The arch's placement, on its own so that the path and anything reasoning
|
|
42
|
+
* about where the text will land come from one calculation.
|
|
43
|
+
*
|
|
44
|
+
* Sizing needs this: it has to know where the renderer will actually put the
|
|
45
|
+
* baseline before it can say whether the text riding it clears the box. Working
|
|
46
|
+
* that out separately is what let the fit engine believe a size fitted while
|
|
47
|
+
* the render clipped.
|
|
48
|
+
*/
|
|
49
|
+
export declare const curvedBaselineGeometry: ({ boxDimensions, baseline, textHeight, verticalAlignment, }: GenerateCurvedBaselineOptions) => CurvedBaselineGeometry;
|
|
31
50
|
/**
|
|
32
51
|
* Generate a path command to curve text and align that text to the top of the box.
|
|
33
52
|
* Uses two quadratic bezier curves — one from the bottom left to the peak, and a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return e.type===`text`}function t(e){let t=e.getState().garment;return e.settings.garments.find(e=>e.slug===t)}function n(e,t,n){if(!e.embellishment_positions)return;let r=e.embellishment_positions.filter(e=>e.placement===t?!n||!e.variant
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return e.type===`text`}function t(e){let t=e.getState().garment;return e.settings.garments.find(e=>e.slug===t)}function n(e,t,n){if(!e.embellishment_positions)return;let r=e.embellishment_positions.filter(e=>e.placement===t?!n||!e.variant||e.variant===n||e.variant.toLowerCase()===`common`:!1);if(n&&r.length>1){let e=r.find(e=>e.variant===n);if(e)return e}return r[0]}function r(e,t){let n=e.fonts.find(e=>e.id===t);if(n)return{id:n.id,fontUrl:n.font_url,fontFamily:n.font_family,letterSpacing:n.letter_spacing??0,letterSpacingOutline:n.letter_spacing_outline??0,outlineWidth:n.outline_width??0,fixedColours:n.fixed_colours}}function i(t,i,a,o,s,c){let{library:l}=s,u=[];for(let[d,f]of Object.entries(o)){if(f.type!==`text`)continue;let o=f,{meta:p}=o;if(!p?.text)continue;let{libraryItemID:m}=o,h=l[m];if(!h||!e(h))continue;let g=p.font;if(g===void 0)continue;let _=r(h,g);if(!_)continue;let v=n(i,d,c);if(!v)continue;let y=null,b=null;if(p.colours&&h.palette!=null)try{let e=t.palettes.getUsedColours(h.palette,p.colours);y=e[0]??null,b=e[1]??null}catch(e){console.warn(`[extractTextPlacements] Colour resolution failed for ${d}:`,e)}let x=s.byId?.[d],S={vertical:`top`,horizontal:`left`};if(x){let e=x;e.vertical_alignment&&(S.vertical=e.vertical_alignment),e.horizontal_alignment&&(S.horizontal=e.horizontal_alignment)}u.push({placementId:d,displayName:d,source:a,text:p.text,font:_,isCurved:p.isCurved??!1,isDistributed:p.isDistributed??!1,verticalAlignment:p.verticalAlignment??S.vertical,horizontalAlignment:p.horizontalAlignment??S.horizontal,rotation:p.rotation??0,spacing:p.spacing??null,fillColour:y,outlineColour:b,position:{width:v.width,height:v.height,physicalWidth:v.physical_width_decimal,physicalHeight:v.physical_height_decimal,physicalUnits:v.physical_size_units,baselinePath:v.baseline_path,rotation:v.rotation},userSetPhysicalSize:p.retainedPhysicalSize??null,fontMetrics:(()=>{let e=p.fontMetrics??{size:0,height:0,baseline:0,width:0},t=e.physicalFontSize||(e.physicalHeight!=null&&e.physicalHeight>0&&e.physicalUnits!=null?[e.physicalHeight,e.physicalUnits]:void 0);return{...e,physicalFontSize:t}})(),manufacturingMethod:p.manufacturing_method??``})}return u}function a(e){let n=e.getState(),r=t(e);if(!r)return console.warn(`[extractTextPlacements] No matching garment found`),[];let a=typeof e.getCurrentVariant==`function`?e.getCurrentVariant():void 0,o=[],s=n.vals?.placements,c=e.settings.placements;s&&c&&o.push(...i(e,r,`placements`,s,c,a));let l=n.vals?.embellishments,u=e.settings.embellishments;return l&&u&&o.push(...i(e,r,`embellishments`,l,u,a)),o.length===0&&console.warn(`[extractTextPlacements] No text entries found in either placements or embellishments`),o}function o(e){return e===`center`?`centre`:e}function s(e){let t=o(e.horizontalAlignment);e.isDistributed&&(t=`distributed`);let n;n=e.position.baselinePath?{type:`custom`,path:e.position.baselinePath}:e.isCurved?`curved`:`flat`;let r=e.userSetPhysicalSize?[e.userSetPhysicalSize[0],e.userSetPhysicalSize[1]]:e.fontMetrics.physicalFontSize==null?[e.position.physicalHeight,e.position.physicalUnits]:e.fontMetrics.physicalFontSize,i={outlineWidth:e.outlineColour==null?0:e.font.outlineWidth,letterSpacing:e.spacing?.letter_spacing??0,letterSpacingOutline:e.spacing?.letter_spacing_outline??0},a=e.fillColour?{hex:e.fillColour.hex,reference:e.fillColour.reference??void 0}:void 0,s=e.outlineColour?{hex:e.outlineColour.hex,reference:e.outlineColour.reference??void 0}:void 0,c;if(e.font.fixedColours){let t={};for(let[n,r]of Object.entries(e.font.fixedColours))t[n]={hex:r.hex,reference:r.reference};c=t}return{text:e.text,fontUrl:e.font.fontUrl,physicalSize:r,fontSizeFallback:e.fontMetrics.size,boxDimensions:{width:e.position.width,height:e.position.height,physicalWidth:e.position.physicalWidth,physicalHeight:e.position.physicalHeight,physicalUnits:e.position.physicalUnits},spacing:i,verticalAlignment:o(e.verticalAlignment),horizontalAlignment:t,baseline:n,fillColour:a,strokeColour:s,colourMap:c}}exports.extractTextPlacements=a,exports.mapStateToRendererOptions=s;
|
package/dist/ce-adapter/index.js
CHANGED
|
@@ -8,7 +8,7 @@ function t(e) {
|
|
|
8
8
|
}
|
|
9
9
|
function n(e, t, n) {
|
|
10
10
|
if (!e.embellishment_positions) return;
|
|
11
|
-
let r = e.embellishment_positions.filter((e) => e.placement === t ? !n || !e.variant
|
|
11
|
+
let r = e.embellishment_positions.filter((e) => e.placement === t ? !n || !e.variant || e.variant === n || e.variant.toLowerCase() === "common" : !1);
|
|
12
12
|
if (n && r.length > 1) {
|
|
13
13
|
let e = r.find((e) => e.variant === n);
|
|
14
14
|
if (e) return e;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { LoadedFont } from '../loading';
|
|
2
|
+
import { BaselineConfig, BaselineType, BoxDimensions, PhysicalSize, TextSpacing, VerticalTextAlignmentValues } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* How the font size is arrived at.
|
|
5
|
+
*
|
|
6
|
+
* `fitToBox` solves for the largest size the box allows. The other two pin the
|
|
7
|
+
* size and leave the text length as the free variable instead — which is what a
|
|
8
|
+
* fixed size preset is: hold 60mm, and fit as many characters as will go. All
|
|
9
|
+
* three then behave identically downstream, so a pinned size is an ordinary mode
|
|
10
|
+
* rather than a special path.
|
|
11
|
+
*/
|
|
12
|
+
export type FitSize = {
|
|
13
|
+
mode: 'fitToBox';
|
|
14
|
+
minFontSize: number;
|
|
15
|
+
maxFontSize: number;
|
|
16
|
+
} | {
|
|
17
|
+
mode: 'fontSize';
|
|
18
|
+
fontSize: number;
|
|
19
|
+
} | {
|
|
20
|
+
mode: 'physicalSize';
|
|
21
|
+
physicalSize: PhysicalSize;
|
|
22
|
+
};
|
|
23
|
+
export interface FitTextToBoxOptions {
|
|
24
|
+
font: LoadedFont;
|
|
25
|
+
text: string;
|
|
26
|
+
boxDimensions: BoxDimensions;
|
|
27
|
+
size: FitSize;
|
|
28
|
+
spacing?: TextSpacing;
|
|
29
|
+
baseline?: BaselineType | BaselineConfig;
|
|
30
|
+
verticalAlignment?: VerticalTextAlignmentValues;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Which limit decided the outcome.
|
|
34
|
+
*
|
|
35
|
+
* - `width` — the box width, or the arc/custom path length for a non-flat baseline
|
|
36
|
+
* - `height` — the box height
|
|
37
|
+
* - `requestedSize` — the size the caller asked for, reached before the box
|
|
38
|
+
* mattered. Covers both hitting `maxFontSize` when solving and simply getting
|
|
39
|
+
* the pinned size that was requested.
|
|
40
|
+
*/
|
|
41
|
+
export type FitConstraint = 'width' | 'height' | 'requestedSize';
|
|
42
|
+
export interface FitTextToBoxResult {
|
|
43
|
+
fontSize: number;
|
|
44
|
+
/** False when the text cannot fit within the box even at minFontSize. */
|
|
45
|
+
fits: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* When `fits`, the limit that capped the size. When not, the limit that could
|
|
48
|
+
* not be satisfied.
|
|
49
|
+
*
|
|
50
|
+
* Callers need this to tell one failure from another: overflowing the width
|
|
51
|
+
* and overflowing the height call for different responses, and in the
|
|
52
|
+
* configuration engine they surface as different messages to the user.
|
|
53
|
+
*/
|
|
54
|
+
boundBy: FitConstraint;
|
|
55
|
+
}
|
|
56
|
+
export interface FitAndTruncateTextToBoxResult extends FitTextToBoxResult {
|
|
57
|
+
text: string;
|
|
58
|
+
truncated: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* How the text was shortened. Reported separately because dropping a line and
|
|
61
|
+
* trimming characters are different outcomes with different messages, and a
|
|
62
|
+
* single pass can do both — drop a line, then still need to trim.
|
|
63
|
+
*/
|
|
64
|
+
linesDropped: number;
|
|
65
|
+
charactersRemoved: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Calculate the font size at which `text` fills `boxDimensions` as closely
|
|
69
|
+
* as possible without overflowing, for any baseline shape. This replaces
|
|
70
|
+
* binary-searching for a fitting font size: flat and custom baselines are
|
|
71
|
+
* solved in one step, from a single measurement, while curved baselines need
|
|
72
|
+
* a bounded numeric solve (see solveCurvedFit).
|
|
73
|
+
*
|
|
74
|
+
* `fits: false` means the text doesn't fit even at minFontSize — the caller
|
|
75
|
+
* (e.g. fitAndTruncateTextToBox) should either shorten the text or accept
|
|
76
|
+
* the overflow at minFontSize.
|
|
77
|
+
*/
|
|
78
|
+
export declare const getFontSizeToFitBox: (options: FitTextToBoxOptions) => FitTextToBoxResult;
|
|
79
|
+
/**
|
|
80
|
+
* Like getFontSizeToFitBox, but when text doesn't fit even at minFontSize,
|
|
81
|
+
* shortens it (dropping the last whole line first if there is more than
|
|
82
|
+
* one, otherwise trimming trailing characters) and retries until it fits
|
|
83
|
+
* or there's nothing left to remove. Mirrors CE's existing truncation UX —
|
|
84
|
+
* text lost this way is a caller/product concern, not a rendering one.
|
|
85
|
+
*/
|
|
86
|
+
export declare const fitAndTruncateTextToBox: (options: FitTextToBoxOptions) => FitAndTruncateTextToBoxResult;
|
package/dist/helpers/bbox.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PlatformMatrix } from '../types';
|
|
1
2
|
export declare class Box {
|
|
2
3
|
corners: DOMPoint[];
|
|
3
4
|
x1: number;
|
|
@@ -13,7 +14,7 @@ export declare class Box {
|
|
|
13
14
|
get x4(): number;
|
|
14
15
|
get y4(): number;
|
|
15
16
|
constructor(x1: number, y1: number, x2: number, y2: number, x3?: number, y3?: number, x4?: number, y4?: number);
|
|
16
|
-
transform(matrix:
|
|
17
|
+
transform(matrix: PlatformMatrix): void;
|
|
17
18
|
toPointsArray(): number[][];
|
|
18
19
|
getBBox(): BBox;
|
|
19
20
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { GetRenderedTextOptions } from './types';
|
|
2
2
|
export { type GenerateCurvedBaselineOptions, type GenerateFlatBaselineOptions, generateCurvedBaseline, generateFlatBaseline, normaliseBaselineOption, validateCustomBaselinePath, } from './baselines';
|
|
3
3
|
export { DEFAULT_OUTLINE_FACTOR, DEFAULT_SPACING, LINE_HEIGHT, } from './constants';
|
|
4
|
+
export { type FitAndTruncateTextToBoxResult, type FitConstraint, type FitSize, type FitTextToBoxOptions, type FitTextToBoxResult, fitAndTruncateTextToBox, getFontSizeToFitBox, } from './fitting';
|
|
4
5
|
export { getFontColours } from './glyph/svg-glyph-converter';
|
|
6
|
+
export { type LayoutTextOptions, layoutText, type MeasuredLine, measureText, type TextLayout, type TextMeasurement, } from './layout';
|
|
5
7
|
export { getFontCache, type LoadedFont, loadFont } from './loading';
|
|
6
8
|
export { type GetFontSizeFromPhysicalSizeOptions, type GetTextMetricsAtFontSizeOptions, getFontSizeFromPhysicalSize, getTextMetricsAtFontSize, } from './measuring';
|
|
7
9
|
export type { GetTextAsPathDataOptions, SvgGlyphEntry } from './rendering';
|