@unmade/text-renderer 1.2.0 → 1.3.1

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 CHANGED
@@ -147,6 +147,53 @@ rendered output. Rotating the frame rather than reasoning about rotation inside
147
147
  solver keeps it working in one coordinate space, and is also what lets a rotated tall
148
148
  placement use its long side for the baseline.
149
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
+
150
197
  ## Configuration Engine adapter
151
198
 
152
199
  The `ce-adapter` sub-package bridges CE (Configuration Engine) editor state to renderer options:
@@ -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||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;
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=e.outlineColour!=null,a=e.spacing??{letter_spacing:e.font.letterSpacing,letter_spacing_outline:e.font.letterSpacingOutline,outline_width:e.font.outlineWidth},s={outlineWidth:i?a.outline_width:0,letterSpacing:a.letter_spacing,letterSpacingOutline:a.letter_spacing_outline},c=e.fillColour?{hex:e.fillColour.hex,reference:e.fillColour.reference??void 0}:void 0,l=e.outlineColour?{hex:e.outlineColour.hex,reference:e.outlineColour.reference??void 0}:void 0,u;if(e.font.fixedColours){let t={};for(let[n,r]of Object.entries(e.font.fixedColours))t[n]={hex:r.hex,reference:r.reference};u=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:s,verticalAlignment:o(e.verticalAlignment),horizontalAlignment:t,baseline:n,fillColour:c,strokeColour:l,colourMap:u}}exports.extractTextPlacements=a,exports.mapStateToRendererOptions=s;
@@ -6,7 +6,7 @@
6
6
  * renderer options. Intended for use in server-side comparison pipelines
7
7
  * and any environment that bridges CE state to the text-renderer.
8
8
  */
9
+ export type { BasePlacementSettings, BasePlacementState, CombinedEditorSettings, ConfigEditor, EditorGarment, EditorPalette, EditorPaletteColour, EditorState, EditorStateVals, EditorVariant, EmbellishmentPosition, Palettes, PlacementLibraryItem, PlacementPositionResult, PlacementsSettings, TextLibraryItem, TextLibraryItemFont, } from './ce-types';
9
10
  export { extractTextPlacements } from './extractTextPlacements';
10
11
  export { mapStateToRendererOptions } from './mapStateToRendererOptions';
11
- export type { BasePlacementSettings, BasePlacementState, CombinedEditorSettings, ConfigEditor, EditorGarment, EditorPalette, EditorPaletteColour, EditorState, EditorStateVals, EditorVariant, EmbellishmentPosition, Palettes, PlacementLibraryItem, PlacementPositionResult, PlacementsSettings, TextLibraryItem, TextLibraryItemFont, } from './ce-types';
12
12
  export type { LoadedTextPlacement, TextPlacementFont, TextPlacementPosition, TextPlacementSpacing, } from './types';
@@ -118,24 +118,28 @@ function s(e) {
118
118
  type: "custom",
119
119
  path: e.position.baselinePath
120
120
  } : e.isCurved ? "curved" : "flat";
121
- let r = e.userSetPhysicalSize ? [e.userSetPhysicalSize[0], e.userSetPhysicalSize[1]] : e.fontMetrics.physicalFontSize == null ? [e.position.physicalHeight, e.position.physicalUnits] : e.fontMetrics.physicalFontSize, i = {
122
- outlineWidth: e.outlineColour == null ? 0 : e.font.outlineWidth,
123
- letterSpacing: e.spacing?.letter_spacing ?? 0,
124
- letterSpacingOutline: e.spacing?.letter_spacing_outline ?? 0
125
- }, a = e.fillColour ? {
121
+ let r = e.userSetPhysicalSize ? [e.userSetPhysicalSize[0], e.userSetPhysicalSize[1]] : e.fontMetrics.physicalFontSize == null ? [e.position.physicalHeight, e.position.physicalUnits] : e.fontMetrics.physicalFontSize, i = e.outlineColour != null, a = e.spacing ?? {
122
+ letter_spacing: e.font.letterSpacing,
123
+ letter_spacing_outline: e.font.letterSpacingOutline,
124
+ outline_width: e.font.outlineWidth
125
+ }, s = {
126
+ outlineWidth: i ? a.outline_width : 0,
127
+ letterSpacing: a.letter_spacing,
128
+ letterSpacingOutline: a.letter_spacing_outline
129
+ }, c = e.fillColour ? {
126
130
  hex: e.fillColour.hex,
127
131
  reference: e.fillColour.reference ?? void 0
128
- } : void 0, s = e.outlineColour ? {
132
+ } : void 0, l = e.outlineColour ? {
129
133
  hex: e.outlineColour.hex,
130
134
  reference: e.outlineColour.reference ?? void 0
131
- } : void 0, c;
135
+ } : void 0, u;
132
136
  if (e.font.fixedColours) {
133
137
  let t = {};
134
138
  for (let [n, r] of Object.entries(e.font.fixedColours)) t[n] = {
135
139
  hex: r.hex,
136
140
  reference: r.reference
137
141
  };
138
- c = t;
142
+ u = t;
139
143
  }
140
144
  return {
141
145
  text: e.text,
@@ -149,13 +153,13 @@ function s(e) {
149
153
  physicalHeight: e.position.physicalHeight,
150
154
  physicalUnits: e.position.physicalUnits
151
155
  },
152
- spacing: i,
156
+ spacing: s,
153
157
  verticalAlignment: o(e.verticalAlignment),
154
158
  horizontalAlignment: t,
155
159
  baseline: n,
156
- fillColour: a,
157
- strokeColour: s,
158
- colourMap: c
160
+ fillColour: c,
161
+ strokeColour: l,
162
+ colourMap: u
159
163
  };
160
164
  }
161
165
  //#endregion
@@ -25,6 +25,7 @@ export interface TextPlacementFont {
25
25
  export interface TextPlacementSpacing {
26
26
  letter_spacing: number;
27
27
  letter_spacing_outline: number;
28
+ outline_width: number;
28
29
  line_height?: number;
29
30
  }
30
31
  export interface LoadedTextPlacement {
@@ -67,9 +67,9 @@ export interface FitAndTruncateTextToBoxResult extends FitTextToBoxResult {
67
67
  /**
68
68
  * Calculate the font size at which `text` fills `boxDimensions` as closely
69
69
  * as possible without overflowing, for any baseline shape. This replaces
70
- * binary-searching for a fitting font size: for flat and custom baselines
71
- * it's a direct O(1) calculation; for curved baselines it's a bounded
72
- * numeric solve (see solveCurvedFit).
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
73
  *
74
74
  * `fits: false` means the text doesn't fit even at minFontSize — the caller
75
75
  * (e.g. fitAndTruncateTextToBox) should either shorten the text or accept
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { type GenerateCurvedBaselineOptions, type GenerateFlatBaselineOptions, g
3
3
  export { DEFAULT_OUTLINE_FACTOR, DEFAULT_SPACING, LINE_HEIGHT, } from './constants';
4
4
  export { type FitAndTruncateTextToBoxResult, type FitConstraint, type FitSize, type FitTextToBoxOptions, type FitTextToBoxResult, fitAndTruncateTextToBox, getFontSizeToFitBox, } from './fitting';
5
5
  export { getFontColours } from './glyph/svg-glyph-converter';
6
+ export { type LayoutTextOptions, layoutText, type MeasuredLine, measureText, type TextLayout, type TextMeasurement, } from './layout';
6
7
  export { getFontCache, type LoadedFont, loadFont } from './loading';
7
8
  export { type GetFontSizeFromPhysicalSizeOptions, type GetTextMetricsAtFontSizeOptions, getFontSizeFromPhysicalSize, getTextMetricsAtFontSize, } from './measuring';
8
9
  export type { GetTextAsPathDataOptions, SvgGlyphEntry } from './rendering';