@unmade/text-renderer 0.1.11 → 0.2.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 +115 -0
- package/dist/ce-adapter/index.cjs +1 -1
- package/dist/ce-adapter/index.js +70 -70
- package/dist/index.cjs +5 -5
- package/dist/index.js +705 -698
- package/dist/node/index.cjs +5 -5
- package/dist/node/index.js +668 -661
- package/dist/worker/index.cjs +15 -15
- package/dist/worker/index.js +1219 -1212
- package/package.json +5 -1
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @unmade/text-renderer
|
|
2
|
+
|
|
3
|
+
Pure function SVG text renderer. No side effects, all dependencies injected explicitly. Web worker compatible.
|
|
4
|
+
|
|
5
|
+
## Public API
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { getRenderedText } from '@unmade/text-renderer';
|
|
9
|
+
|
|
10
|
+
const svg = await getRenderedText({
|
|
11
|
+
text: 'HELLO',
|
|
12
|
+
fontUrl: 'https://example.com/font.ttf',
|
|
13
|
+
physicalSize: [40, 'mm'],
|
|
14
|
+
boxDimensions: {
|
|
15
|
+
width: 500,
|
|
16
|
+
height: 200,
|
|
17
|
+
physicalWidth: 100,
|
|
18
|
+
physicalHeight: 40,
|
|
19
|
+
physicalUnits: 'mm',
|
|
20
|
+
},
|
|
21
|
+
spacing: { outlineWidth: 0, letterSpacing: 0, letterSpacingOutline: 0 },
|
|
22
|
+
baseline: 'flat', // or 'curved', or { type: 'custom', path: '...' }
|
|
23
|
+
horizontalAlignment: 'centre', // 'left' | 'centre' | 'right' | 'distributed'
|
|
24
|
+
verticalAlignment: 'centre', // 'top' | 'centre' | 'bottom'
|
|
25
|
+
fillColour: { hex: '#000000' },
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Import paths
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { getRenderedText } from '@unmade/text-renderer'; // Browser
|
|
33
|
+
import { getRenderedText } from '@unmade/text-renderer/node'; // Node.js
|
|
34
|
+
import { getRenderedText } from '@unmade/text-renderer/worker'; // Web Worker
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration Engine adapter
|
|
38
|
+
|
|
39
|
+
The `ce-adapter` sub-package bridges CE (Configuration Engine) editor state to renderer options:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { extractTextPlacements, mapStateToRendererOptions } from '@unmade/text-renderer/ce-adapter';
|
|
43
|
+
|
|
44
|
+
const placements = extractTextPlacements(editor); // LoadedTextPlacement[]
|
|
45
|
+
const options = mapStateToRendererOptions(placements[0]); // GetRenderedTextOptions
|
|
46
|
+
const svg = await getRenderedText(options);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`extractTextPlacements` reads an editor instance and returns one `LoadedTextPlacement` per text placement, resolving fonts, colours, and position data from the CE state.
|
|
50
|
+
|
|
51
|
+
`mapStateToRendererOptions` converts a `LoadedTextPlacement` into the `GetRenderedTextOptions` shape that `getRenderedText` accepts — including alignment normalisation (CE uses American English, TR uses British English).
|
|
52
|
+
|
|
53
|
+
## Comparison system
|
|
54
|
+
|
|
55
|
+
The `packages/tr-comparison` package provides a pipeline for validating TR output against the CE's own rendering for real production designs.
|
|
56
|
+
|
|
57
|
+
### How it works
|
|
58
|
+
|
|
59
|
+
1. A design URL is POSTed to the comparison Lambda
|
|
60
|
+
2. The Lambda loads the design via the CE factory, extracts all text placements, and renders each one with both CE and TR
|
|
61
|
+
3. Both outputs are rasterised to PNG and diffed pixel-by-pixel with pixelmatch
|
|
62
|
+
4. Results (match score, images, state) are stored in S3 and indexed in DynamoDB
|
|
63
|
+
5. A browser comparison is also run via Puppeteer against the deployed demo app
|
|
64
|
+
6. The comparison dashboard at `packages/tr-comparison/src` displays all results
|
|
65
|
+
|
|
66
|
+
### Stored S3 artefacts
|
|
67
|
+
|
|
68
|
+
For each placement comparison:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
comparisons/{designUrlHash}/{timestamp}/{placementId}/
|
|
72
|
+
ce.png # Config Engine render
|
|
73
|
+
tr.png # Text Renderer render
|
|
74
|
+
diff.png # Pixel difference heatmap
|
|
75
|
+
state.json # LoadedTextPlacement (CE state passed to the adapter)
|
|
76
|
+
renderer-options.json # GetRenderedTextOptions (mapped TR input)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Regression tests
|
|
80
|
+
|
|
81
|
+
Regression tests live in `__tests__/regression.test.ts` and use fixture pairs in `__tests__/fixtures/regression/`:
|
|
82
|
+
|
|
83
|
+
| File | Contents |
|
|
84
|
+
|------|----------|
|
|
85
|
+
| `{name}.json` | Fixture metadata + `LoadedTextPlacement` |
|
|
86
|
+
| `{name}.png` | Reference PNG (expected TR render) |
|
|
87
|
+
|
|
88
|
+
The test pipeline for each fixture:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
LoadedTextPlacement
|
|
92
|
+
→ mapStateToRendererOptions()
|
|
93
|
+
→ getRenderedText()
|
|
94
|
+
→ SVG → PNG (resvg-js)
|
|
95
|
+
→ pixelmatch diff against {name}.png
|
|
96
|
+
→ fail if diff > 1%
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Adding a fixture
|
|
100
|
+
|
|
101
|
+
1. Run a comparison in the dashboard against a design URL
|
|
102
|
+
2. Select the record and click **Save as fixture** in the detail panel
|
|
103
|
+
3. Enter a descriptive name (e.g. `curved-jersey-40mm`) — two files download
|
|
104
|
+
4. Place both files in `__tests__/fixtures/regression/`
|
|
105
|
+
5. Run `npm test` to confirm the fixture passes, then commit
|
|
106
|
+
|
|
107
|
+
### Updating fixtures after an intentional change
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
UPDATE_FIXTURES=true npm test
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This re-renders all fixtures and overwrites the stored PNGs. Review the diffs, then commit.
|
|
114
|
+
|
|
115
|
+
> Regression tests require network access to fetch fonts from their CDN URLs.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function C(t){return t.type==="text"}function P(t){const i=t.getState().garment;return t.settings.garments.find(s=>s.slug===i)}function
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function C(t){return t.type==="text"}function P(t){const i=t.getState().garment;return t.settings.garments.find(s=>s.slug===i)}function w(t,o){if(t.embellishment_positions)return t.embellishment_positions.find(i=>i.placement===o)}function A(t,o){const i=t.fonts.find(s=>s.id===o);if(i)return{id:i.id,fontUrl:i.font_url,fontFamily:i.font_family,letterSpacing:i.letter_spacing??0,letterSpacingOutline:i.letter_spacing_outline??0,outlineWidth:i.outline_width??0,fixedColours:i.fixed_colours}}function v(t,o,i,s,c){const{library:u}=c,a=[];for(const[l,h]of Object.entries(s)){if(h.type!=="text")continue;const f=h,{meta:n}=f;if(!n?.text)continue;const{libraryItemID:d}=f,g=u[d];if(!g||!C(g))continue;const y=n.font;if(y===void 0)continue;const m=A(g,y);if(!m)continue;const r=w(o,l);if(!r)continue;let x=null,b=null;if(n.colours&&g.palette!=null)try{const e=t.palettes.getUsedColours(g.palette,n.colours);x=e[0]??null,b=e[1]??null}catch(e){console.warn(`[extractTextPlacements] Colour resolution failed for ${l}:`,e)}const S=c.byId?.[l],p={vertical:"top",horizontal:"left"};if(S){const e=S;e.vertical_alignment&&(p.vertical=e.vertical_alignment),e.horizontal_alignment&&(p.horizontal=e.horizontal_alignment)}a.push({placementId:l,displayName:l,source:i,text:n.text,font:m,isCurved:n.isCurved??!1,isDistributed:n.isDistributed??!1,verticalAlignment:n.verticalAlignment??p.vertical,horizontalAlignment:n.horizontalAlignment??p.horizontal,rotation:n.rotation??0,spacing:n.spacing??null,fillColour:x,outlineColour:b,position:{width:r.width,height:r.height,physicalWidth:r.physical_width_decimal,physicalHeight:r.physical_height_decimal,physicalUnits:r.physical_size_units,baselinePath:r.baseline_path,rotation:r.rotation},userSetPhysicalSize:n.retainedPhysicalSize??null,fontMetrics:(()=>{const e=n.fontMetrics??{size:0,height:0,baseline:0,width:0},z=e.physicalFontSize||(e.physicalHeight!=null&&e.physicalHeight>0&&e.physicalUnits!=null?[e.physicalHeight,e.physicalUnits]:void 0);return{...e,physicalFontSize:z}})(),manufacturingMethod:n.manufacturing_method??""})}return a}function U(t){const o=t.getState(),i=P(t);if(!i)return console.warn("[extractTextPlacements] No matching garment found"),[];const s=[],c=o.vals?.placements,u=t.settings.placements;c&&u&&s.push(...v(t,i,"placements",c,u));const a=o.vals?.embellishments,l=t.settings.embellishments;return a&&l&&s.push(...v(t,i,"embellishments",a,l)),s.length===0&&console.warn("[extractTextPlacements] No text entries found in either placements or embellishments"),s}function _(t){return t==="center"?"centre":t}function O(t){let o=_(t.horizontalAlignment);t.isDistributed&&(o="distributed");let i;t.position.baselinePath?i={type:"custom",path:t.position.baselinePath}:t.isCurved?i="curved":i="flat";const s=t.userSetPhysicalSize?[t.userSetPhysicalSize[0],t.userSetPhysicalSize[1]]:t.fontMetrics.physicalFontSize!=null?t.fontMetrics.physicalFontSize:[t.position.physicalHeight,t.position.physicalUnits],u={outlineWidth:t.outlineColour!=null?t.font.outlineWidth:0,letterSpacing:t.spacing?.letter_spacing??0,letterSpacingOutline:t.spacing?.letter_spacing_outline??0},a=t.fillColour?{hex:t.fillColour.hex,reference:t.fillColour.reference??void 0}:void 0,l=t.outlineColour?{hex:t.outlineColour.hex,reference:t.outlineColour.reference??void 0}:void 0;let h;if(t.font.fixedColours){const f={};for(const[n,d]of Object.entries(t.font.fixedColours))f[n]={hex:d.hex,reference:d.reference};h=f}return{text:t.text,fontUrl:t.font.fontUrl,physicalSize:s,fontSizeFallback:t.fontMetrics.size,boxDimensions:{width:t.position.width,height:t.position.height,physicalWidth:t.position.physicalWidth,physicalHeight:t.position.physicalHeight,physicalUnits:t.position.physicalUnits},spacing:u,verticalAlignment:_(t.verticalAlignment),horizontalAlignment:o,baseline:i,fillColour:a,strokeColour:l,colourMap:h}}exports.extractTextPlacements=U;exports.mapStateToRendererOptions=O;
|
package/dist/ce-adapter/index.js
CHANGED
|
@@ -3,16 +3,16 @@ function C(t) {
|
|
|
3
3
|
}
|
|
4
4
|
function P(t) {
|
|
5
5
|
const i = t.getState().garment;
|
|
6
|
-
return t.settings.garments.find((
|
|
6
|
+
return t.settings.garments.find((s) => s.slug === i);
|
|
7
7
|
}
|
|
8
|
-
function
|
|
8
|
+
function w(t, o) {
|
|
9
9
|
if (t.embellishment_positions)
|
|
10
10
|
return t.embellishment_positions.find(
|
|
11
|
-
(i) => i.placement ===
|
|
11
|
+
(i) => i.placement === o
|
|
12
12
|
);
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
const i = t.fonts.find((
|
|
14
|
+
function A(t, o) {
|
|
15
|
+
const i = t.fonts.find((s) => s.id === o);
|
|
16
16
|
if (i)
|
|
17
17
|
return {
|
|
18
18
|
id: i.id,
|
|
@@ -24,51 +24,51 @@ function w(t, s) {
|
|
|
24
24
|
fixedColours: i.fixed_colours
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
-
function
|
|
28
|
-
const { library:
|
|
29
|
-
for (const [l,
|
|
30
|
-
if (
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
33
|
-
const { libraryItemID:
|
|
34
|
-
if (!
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
const
|
|
38
|
-
if (!
|
|
39
|
-
const r =
|
|
27
|
+
function v(t, o, i, s, c) {
|
|
28
|
+
const { library: u } = c, a = [];
|
|
29
|
+
for (const [l, h] of Object.entries(s)) {
|
|
30
|
+
if (h.type !== "text") continue;
|
|
31
|
+
const f = h, { meta: n } = f;
|
|
32
|
+
if (!n?.text) continue;
|
|
33
|
+
const { libraryItemID: d } = f, g = u[d];
|
|
34
|
+
if (!g || !C(g)) continue;
|
|
35
|
+
const y = n.font;
|
|
36
|
+
if (y === void 0) continue;
|
|
37
|
+
const m = A(g, y);
|
|
38
|
+
if (!m) continue;
|
|
39
|
+
const r = w(o, l);
|
|
40
40
|
if (!r) continue;
|
|
41
41
|
let x = null, b = null;
|
|
42
|
-
if (
|
|
42
|
+
if (n.colours && g.palette != null)
|
|
43
43
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const e = t.palettes.getUsedColours(
|
|
45
|
+
g.palette,
|
|
46
|
+
n.colours
|
|
47
47
|
);
|
|
48
|
-
x =
|
|
49
|
-
} catch (
|
|
50
|
-
console.warn(`[extractTextPlacements] Colour resolution failed for ${l}:`,
|
|
48
|
+
x = e[0] ?? null, b = e[1] ?? null;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.warn(`[extractTextPlacements] Colour resolution failed for ${l}:`, e);
|
|
51
51
|
}
|
|
52
|
-
const
|
|
52
|
+
const S = c.byId?.[l], p = {
|
|
53
53
|
vertical: "top",
|
|
54
54
|
horizontal: "left"
|
|
55
55
|
};
|
|
56
|
-
if (
|
|
57
|
-
const
|
|
58
|
-
|
|
56
|
+
if (S) {
|
|
57
|
+
const e = S;
|
|
58
|
+
e.vertical_alignment && (p.vertical = e.vertical_alignment), e.horizontal_alignment && (p.horizontal = e.horizontal_alignment);
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
a.push({
|
|
61
61
|
placementId: l,
|
|
62
62
|
displayName: l,
|
|
63
63
|
source: i,
|
|
64
|
-
text:
|
|
65
|
-
font:
|
|
66
|
-
isCurved:
|
|
67
|
-
isDistributed:
|
|
68
|
-
verticalAlignment:
|
|
69
|
-
horizontalAlignment:
|
|
70
|
-
rotation:
|
|
71
|
-
spacing:
|
|
64
|
+
text: n.text,
|
|
65
|
+
font: m,
|
|
66
|
+
isCurved: n.isCurved ?? !1,
|
|
67
|
+
isDistributed: n.isDistributed ?? !1,
|
|
68
|
+
verticalAlignment: n.verticalAlignment ?? p.vertical,
|
|
69
|
+
horizontalAlignment: n.horizontalAlignment ?? p.horizontal,
|
|
70
|
+
rotation: n.rotation ?? 0,
|
|
71
|
+
spacing: n.spacing ?? null,
|
|
72
72
|
fillColour: x,
|
|
73
73
|
outlineColour: b,
|
|
74
74
|
position: {
|
|
@@ -80,78 +80,78 @@ function z(t, s, i, o, h) {
|
|
|
80
80
|
baselinePath: r.baseline_path,
|
|
81
81
|
rotation: r.rotation
|
|
82
82
|
},
|
|
83
|
-
userSetPhysicalSize:
|
|
83
|
+
userSetPhysicalSize: n.retainedPhysicalSize ?? null,
|
|
84
84
|
fontMetrics: (() => {
|
|
85
|
-
const
|
|
86
|
-
return { ...
|
|
85
|
+
const e = n.fontMetrics ?? { size: 0, height: 0, baseline: 0, width: 0 }, z = e.physicalFontSize || (e.physicalHeight != null && e.physicalHeight > 0 && e.physicalUnits != null ? [e.physicalHeight, e.physicalUnits] : void 0);
|
|
86
|
+
return { ...e, physicalFontSize: z };
|
|
87
87
|
})(),
|
|
88
|
-
manufacturingMethod:
|
|
88
|
+
manufacturingMethod: n.manufacturing_method ?? ""
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
-
return
|
|
91
|
+
return a;
|
|
92
92
|
}
|
|
93
93
|
function U(t) {
|
|
94
|
-
const
|
|
94
|
+
const o = t.getState(), i = P(t);
|
|
95
95
|
if (!i)
|
|
96
96
|
return console.warn("[extractTextPlacements] No matching garment found"), [];
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
return
|
|
97
|
+
const s = [], c = o.vals?.placements, u = t.settings.placements;
|
|
98
|
+
c && u && s.push(...v(t, i, "placements", c, u));
|
|
99
|
+
const a = o.vals?.embellishments, l = t.settings.embellishments;
|
|
100
|
+
return a && l && s.push(...v(t, i, "embellishments", a, l)), s.length === 0 && console.warn("[extractTextPlacements] No text entries found in either placements or embellishments"), s;
|
|
101
101
|
}
|
|
102
|
-
function
|
|
102
|
+
function _(t) {
|
|
103
103
|
return t === "center" ? "centre" : t;
|
|
104
104
|
}
|
|
105
|
-
function
|
|
106
|
-
let
|
|
107
|
-
t.isDistributed && (
|
|
105
|
+
function F(t) {
|
|
106
|
+
let o = _(t.horizontalAlignment);
|
|
107
|
+
t.isDistributed && (o = "distributed");
|
|
108
108
|
let i;
|
|
109
109
|
t.position.baselinePath ? i = { type: "custom", path: t.position.baselinePath } : t.isCurved ? i = "curved" : i = "flat";
|
|
110
|
-
const
|
|
110
|
+
const s = t.userSetPhysicalSize ? [
|
|
111
111
|
t.userSetPhysicalSize[0],
|
|
112
112
|
t.userSetPhysicalSize[1]
|
|
113
|
-
] : t.fontMetrics.physicalFontSize != null ? t.fontMetrics.physicalFontSize : [t.position.physicalHeight, t.position.physicalUnits],
|
|
113
|
+
] : t.fontMetrics.physicalFontSize != null ? t.fontMetrics.physicalFontSize : [t.position.physicalHeight, t.position.physicalUnits], u = {
|
|
114
114
|
outlineWidth: t.outlineColour != null ? t.font.outlineWidth : 0,
|
|
115
115
|
letterSpacing: t.spacing?.letter_spacing ?? 0,
|
|
116
116
|
letterSpacingOutline: t.spacing?.letter_spacing_outline ?? 0
|
|
117
|
-
},
|
|
117
|
+
}, a = t.fillColour ? {
|
|
118
118
|
hex: t.fillColour.hex,
|
|
119
119
|
reference: t.fillColour.reference ?? void 0
|
|
120
|
-
} : void 0,
|
|
120
|
+
} : void 0, l = t.outlineColour ? {
|
|
121
121
|
hex: t.outlineColour.hex,
|
|
122
122
|
reference: t.outlineColour.reference ?? void 0
|
|
123
123
|
} : void 0;
|
|
124
|
-
let
|
|
124
|
+
let h;
|
|
125
125
|
if (t.font.fixedColours) {
|
|
126
|
-
const
|
|
127
|
-
for (const [
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
const f = {};
|
|
127
|
+
for (const [n, d] of Object.entries(t.font.fixedColours))
|
|
128
|
+
f[n] = { hex: d.hex, reference: d.reference };
|
|
129
|
+
h = f;
|
|
130
130
|
}
|
|
131
131
|
return {
|
|
132
132
|
text: t.text,
|
|
133
133
|
fontUrl: t.font.fontUrl,
|
|
134
|
-
physicalSize:
|
|
134
|
+
physicalSize: s,
|
|
135
135
|
fontSizeFallback: t.fontMetrics.size,
|
|
136
136
|
boxDimensions: {
|
|
137
137
|
width: t.position.width,
|
|
138
138
|
height: t.position.height,
|
|
139
139
|
physicalWidth: t.position.physicalWidth,
|
|
140
|
-
physicalHeight:
|
|
140
|
+
physicalHeight: t.position.physicalHeight,
|
|
141
141
|
physicalUnits: t.position.physicalUnits
|
|
142
142
|
},
|
|
143
|
-
spacing:
|
|
144
|
-
verticalAlignment:
|
|
143
|
+
spacing: u,
|
|
144
|
+
verticalAlignment: _(
|
|
145
145
|
t.verticalAlignment
|
|
146
146
|
),
|
|
147
|
-
horizontalAlignment:
|
|
147
|
+
horizontalAlignment: o,
|
|
148
148
|
baseline: i,
|
|
149
|
-
fillColour:
|
|
150
|
-
strokeColour:
|
|
151
|
-
colourMap:
|
|
149
|
+
fillColour: a,
|
|
150
|
+
strokeColour: l,
|
|
151
|
+
colourMap: h
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
export {
|
|
155
155
|
U as extractTextPlacements,
|
|
156
|
-
|
|
156
|
+
F as mapStateToRendererOptions
|
|
157
157
|
};
|