@richardmcquiston01/unofficial-xcs-writer 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.1] - 2026-09-19
9
+
10
+ ### Changed
11
+
12
+ - Version bump only — 0.4.0 itself was never published to npm (only consumed
13
+ ahead of release via a git dependency from `maker-toolkit`'s `apps/desktop`,
14
+ which pointed at this repo's `dist` branch as a workaround). No code changes
15
+ from 0.4.0; this is the first published release carrying the generation API
16
+ and curved-text layout described below.
17
+
18
+ ## [0.4.0] - 2026-09-19
19
+
20
+ ### Added
21
+
22
+ - **Generation API** (`src/builder.ts`): `XCSGenerator`/`createXCS` build a `.xcs` project from
23
+ scratch — canvas, layers, and `TEXT`/`PATH`/`BITMAP` displays — ported from `maker-toolkit`'s
24
+ previously-duplicated hand-rolled generator. Chainable `.addText()`/`.addPath()`/
25
+ `.addBitmap()`/`.addLayer()`, then `.generate()`/`.toJSON()`/`.toBytes()`.
26
+ - **Curved text layout** (`src/layout.ts`): `layoutCurvedGlyphText` lays a single line of text
27
+ out along a circular arc, baking each character's rotated position and glyph shape directly
28
+ (same approach `renderXcsFile` already uses for straight substituted text — xTool Studio only
29
+ trusts stored glyph shapes). Uses the same arc-geometry convention (chord width, sweep angle,
30
+ up/down sign, align-driven start offset) as SVG `<textPath>`/canvas curved-text editors, so a
31
+ document renders consistently across export formats. Not yet visually verified against real
32
+ xTool Studio output — see `CLAUDE.md`'s "Open Issues".
33
+ - `layoutGlyphText`/`layoutMultilineGlyphText`/`translateGlyphLayout`/`fontSizePoints`
34
+ (`src/layout.ts`): the higher-level, real-world-scaled/positioned layout helpers built on
35
+ `glyphs.ts`'s `layoutText`, also ported from `maker-toolkit` (previously duplicated
36
+ consumer-side). `effectiveCurveDeg` clamps/normalizes a curve angle (±355°, treats <1° as
37
+ straight).
38
+
39
+ ### Changed
40
+
41
+ - README's substitution "Known limitation" note narrowed to `renderXcsFile` specifically — it
42
+ still can't decode an existing curved display's `style.curveX`/`curveY`, but the new building
43
+ API doesn't have that problem (the caller supplies the angle directly).
44
+
45
+ ## [0.3.1] - 2026-08-17
46
+
47
+ ### Changed
48
+
49
+ - Replaced the ad-hoc PayPal "Support" section in the README with the standard
50
+ "Buy Me a Coffee" donation block (Stripe link plus a scannable QR code at
51
+ `donate.svg`), now the last section of the README.
52
+ - `donate.svg` is included in the published tarball so the README's QR image
53
+ resolves for consumers reading it from the package.
54
+
8
55
  ## [0.3.0] - 2026-07-14
9
56
 
10
57
  ### Added
package/README.md CHANGED
@@ -1,123 +1,191 @@
1
- # unofficial-xcs-writer
2
-
3
- A framework-agnostic TypeScript library for reading, writing, and applying variable substitution to `.xcs` files produced by [xTool Creative Space](https://www.xtool.com/pages/software). Not affiliated with xTool.
4
-
5
- ## Support
6
-
7
- If this library saved you some reverse-engineering, consider [buying me a coffee](https://www.paypal.com/ncp/payment/VDTESHTRR7684). ☕
8
-
9
- ## What is an XCS file?
10
-
11
- An `.xcs` file is a plain UTF-8 JSON document exported by xTool Creative Space. It describes a laser project — canvas dimensions, design objects (shapes, text, images), layer assignments, and device parameters. This library lets you:
12
-
13
- - **Read** an XCS file and inspect its contents
14
- - **Extract** `{{token}}` template placeholders from text objects
15
- - **Render** a filled-in copy by substituting values for those placeholders
16
-
17
- ## Installation
18
-
19
- ```bash
20
- npm install @richardmcquiston01/unofficial-xcs-writer
21
- # or
22
- pnpm add @richardmcquiston01/unofficial-xcs-writer
23
- ```
24
-
25
- ## Usage
26
-
27
- ```ts
28
- import { assertXcsFormat, extractXcsTokens, renderXcsFile } from '@richardmcquiston01/unofficial-xcs-writer';
29
- import type { XcsVariable } from '@richardmcquiston01/unofficial-xcs-writer';
30
-
31
- // Load an .xcs file as an ArrayBuffer (browser or Node.js)
32
- const buffer = await fetch('/templates/name-tag.xcs').then(r => r.arrayBuffer());
33
-
34
- // Validate it's a real XCS file
35
- assertXcsFormat(buffer); // throws if invalid
36
-
37
- // Discover which {{token}} placeholders are in the file
38
- const tokens = extractXcsTokens(buffer);
39
- // e.g. ['FirstName', 'LastName']
40
-
41
- // Define variables and supply values
42
- const variables: XcsVariable[] = [
43
- { token: 'FirstName', defaultValue: 'Guest' },
44
- { token: 'LastName' },
45
- ];
46
- const values = { FirstName: 'Jane', LastName: 'Smith' };
47
-
48
- // Produce a filled-in .xcs file ready for download
49
- const output: Uint8Array = renderXcsFile(buffer, variables, values);
50
- ```
51
-
52
- ## API
53
-
54
- ### `assertXcsFormat(buffer: ArrayBuffer): void`
55
-
56
- Validates that `buffer` is a parseable XCS JSON file containing a `canvas` array. Throws a descriptive error if the file is invalid or unrecognized.
57
-
58
- ### `readXcsFile(buffer: ArrayBuffer): string`
59
-
60
- Decodes the buffer and returns the raw JSON string. Useful for inspection or pass-through scenarios.
61
-
62
- ### `extractXcsTokens(buffer: ArrayBuffer): string[]`
63
-
64
- Scans all `TEXT` display objects across all canvases and returns the unique token names found (without braces). For example, `{{LastName}}` in the file → `"LastName"` in the returned array.
65
-
66
- ### `renderXcsFile(buffer, variables, values, fonts?): Uint8Array`
67
-
68
- Applies substitution to every `TEXT` display object and returns the modified file encoded as a UTF-8 `Uint8Array`. Tokens with no matching entry in `values` fall back to `variable.defaultValue`, or an empty string if no default is set.
69
-
70
- Whenever a display's text actually changes, its glyph outline data (`charJSONs` / `fontData.glyphData`) is regenerated from a real font so xTool Studio renders the new text correctly — xTool Studio only trusts a TEXT display's stored glyph shapes; it does not re-shape text on its own unless you manually reselect the font inside the app. Displays whose text doesn't change are left completely untouched.
71
-
72
- Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
73
-
74
- ```ts
75
- const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
76
- renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
77
- ```
78
-
79
- Any `fontFamily` not present in `fonts` falls back to a bundled default (Arimo, Apache-2.0, metric-compatible with Arial) — so the fix works out of the box with no font files required, just with shapes drawn from Arimo rather than the exact requested typeface.
80
-
81
- **Known limitation:** only straight horizontal-baseline layout is reproduced. xTool Studio's `style.curveX`/`curveY` curved-text layout formula is undocumented and isn't replicated — substituted text on a curved TEXT display gets correctly-shaped glyphs laid out on a straight line instead of the original curve.
82
-
83
- ### `XcsVariable`
84
-
85
- ```ts
86
- interface XcsVariable {
87
- token: string; // token name without braces, e.g. "LastName"
88
- defaultValue?: string;
89
- }
90
- ```
91
-
92
- ### `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
93
-
94
- Lower-level glyph-extraction primitives `renderXcsFile` is built on, exported for direct use (e.g. building a `TEXT` display from scratch). See `src/glyphs.ts` for the exact xTool JSON conventions these were reverse-engineered against.
95
-
96
- ## Development
97
-
98
- ```bash
99
- pnpm install # install dev dependencies
100
- pnpm build # compile → dist/ (ESM + CJS + .d.ts)
101
- pnpm dev # watch mode — rebuilds on save
102
- pnpm test # run tests with vitest
103
- npx tsc --noEmit # type-check without emitting
104
- ```
105
-
106
- Output files after `pnpm build`:
107
-
108
- | File | Format |
109
- |------|--------|
110
- | `dist/index.js` | ESM |
111
- | `dist/index.cjs` | CommonJS |
112
- | `dist/index.d.ts` | TypeScript declarations |
113
-
114
- ## Notes
115
-
116
- - All inputs and outputs use plain browser-compatible types (`ArrayBuffer`, `Uint8Array`, `string`) — no Node.js APIs, no DOM, no framework required. (`atob`/`crypto.randomUUID` are used for the bundled font and glyph IDs; both are available in browsers and Node ≥ 19.)
117
- - Only `TEXT` display objects are modified during substitution. Geometry, bitmaps, device configuration, and all other fields pass through unchanged.
118
- - This library targets the current xTool Creative Space JSON format. Older versions of the software may export a different structure.
119
- - Glyph outline extraction is powered by [opentype.js](https://github.com/opentypejs/opentype.js). The bundled fallback font is [Arimo](https://fonts.google.com/specimen/Arimo) v5.2.8 via [`@fontsource/arimo`](https://www.npmjs.com/package/@fontsource/arimo) (Apache License 2.0, see `third_party/arimo/LICENSE`).
120
-
121
- ## License
122
-
123
- [MIT](LICENSE). The bundled Arimo fallback font is separately licensed under Apache-2.0 (`third_party/arimo/LICENSE`).
1
+ # unofficial-xcs-writer
2
+
3
+ A framework-agnostic TypeScript library for reading, writing, building, and applying variable substitution to `.xcs` files produced by [xTool Creative Space](https://www.xtool.com/pages/software). Not affiliated with xTool.
4
+
5
+ ## What is an XCS file?
6
+
7
+ An `.xcs` file is a plain UTF-8 JSON document exported by xTool Creative Space. It describes a laser project — canvas dimensions, design objects (shapes, text, images), layer assignments, and device parameters. This library lets you:
8
+
9
+ - **Read** an XCS file and inspect its contents
10
+ - **Extract** `{{token}}` template placeholders from text objects
11
+ - **Render** a filled-in copy by substituting values for those placeholders
12
+ - **Build** a new `.xcs` project from scratch — text (straight, multi-line, or curved), paths, and embedded images
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @richardmcquiston01/unofficial-xcs-writer
18
+ # or
19
+ pnpm add @richardmcquiston01/unofficial-xcs-writer
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { assertXcsFormat, extractXcsTokens, renderXcsFile } from '@richardmcquiston01/unofficial-xcs-writer';
26
+ import type { XcsVariable } from '@richardmcquiston01/unofficial-xcs-writer';
27
+
28
+ // Load an .xcs file as an ArrayBuffer (browser or Node.js)
29
+ const buffer = await fetch('/templates/name-tag.xcs').then(r => r.arrayBuffer());
30
+
31
+ // Validate it's a real XCS file
32
+ assertXcsFormat(buffer); // throws if invalid
33
+
34
+ // Discover which {{token}} placeholders are in the file
35
+ const tokens = extractXcsTokens(buffer);
36
+ // e.g. ['FirstName', 'LastName']
37
+
38
+ // Define variables and supply values
39
+ const variables: XcsVariable[] = [
40
+ { token: 'FirstName', defaultValue: 'Guest' },
41
+ { token: 'LastName' },
42
+ ];
43
+ const values = { FirstName: 'Jane', LastName: 'Smith' };
44
+
45
+ // Produce a filled-in .xcs file ready for download
46
+ const output: Uint8Array = renderXcsFile(buffer, variables, values);
47
+ ```
48
+
49
+ ### Building a project from scratch
50
+
51
+ ```ts
52
+ import {
53
+ createXCS,
54
+ loadDefaultFont,
55
+ layoutGlyphText,
56
+ layoutCurvedGlyphText,
57
+ } from '@richardmcquiston01/unofficial-xcs-writer';
58
+
59
+ const font = loadDefaultFont();
60
+
61
+ const project = createXCS('P2S')
62
+ // Straight text needs a real glyph layout so xTool Studio renders
63
+ // actual outlines instead of placeholder blocks.
64
+ .addText('Hello', 10, 10, {
65
+ fontFamily: 'Arial',
66
+ fontSize: 24,
67
+ layout: layoutGlyphText(font, 'Hello', 8, 10, 10),
68
+ })
69
+ // Curved text: same idea, via layoutCurvedGlyphText -- the curve
70
+ // is baked into each character's own glyph shape and position, the
71
+ // way substituted text already bakes straight shapes.
72
+ .addText('Arch Text', 0, 40, {
73
+ fontFamily: 'Arial',
74
+ fontSize: 18,
75
+ layout: layoutCurvedGlyphText(font, 'Arch Text', 6, 90, 60, 20, 'center'),
76
+ })
77
+ .addPath('M0 0L10 0L10 10L0 10Z', 0, 60, 10, 10)
78
+ .toBytes(); // Uint8Array, ready to write to a .xcs file
79
+ ```
80
+
81
+ ## API
82
+
83
+ ### `assertXcsFormat(buffer: ArrayBuffer): void`
84
+
85
+ Validates that `buffer` is a parseable XCS JSON file containing a `canvas` array. Throws a descriptive error if the file is invalid or unrecognized.
86
+
87
+ ### `readXcsFile(buffer: ArrayBuffer): string`
88
+
89
+ Decodes the buffer and returns the raw JSON string. Useful for inspection or pass-through scenarios.
90
+
91
+ ### `extractXcsTokens(buffer: ArrayBuffer): string[]`
92
+
93
+ Scans all `TEXT` display objects across all canvases and returns the unique token names found (without braces). For example, `{{LastName}}` in the file → `"LastName"` in the returned array.
94
+
95
+ ### `renderXcsFile(buffer, variables, values, fonts?): Uint8Array`
96
+
97
+ Applies substitution to every `TEXT` display object and returns the modified file encoded as a UTF-8 `Uint8Array`. Tokens with no matching entry in `values` fall back to `variable.defaultValue`, or an empty string if no default is set.
98
+
99
+ Whenever a display's text actually changes, its glyph outline data (`charJSONs` / `fontData.glyphData`) is regenerated from a real font so xTool Studio renders the new text correctly — xTool Studio only trusts a TEXT display's stored glyph shapes; it does not re-shape text on its own unless you manually reselect the font inside the app. Displays whose text doesn't change are left completely untouched.
100
+
101
+ Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
102
+
103
+ ```ts
104
+ const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
105
+ renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
106
+ ```
107
+
108
+ Any `fontFamily` not present in `fonts` falls back to a bundled default (Arimo, Apache-2.0, metric-compatible with Arial) — so the fix works out of the box with no font files required, just with shapes drawn from Arimo rather than the exact requested typeface.
109
+
110
+ **Known limitation:** `renderXcsFile` only reproduces straight horizontal-baseline layout when substituting into an *existing* TEXT display. xTool Studio's `style.curveX`/`curveY` curved-text formula is undocumented and can't be reverse-decoded from a file alone, so a curved display's substituted text gets correctly-shaped glyphs laid out straight instead of on the original curve. This doesn't apply to the *building* API below — there, you supply the curve angle yourself (see `layoutCurvedGlyphText`), so there's nothing to decode.
111
+
112
+ ### `XcsVariable`
113
+
114
+ ```ts
115
+ interface XcsVariable {
116
+ token: string; // token name without braces, e.g. "LastName"
117
+ defaultValue?: string;
118
+ }
119
+ ```
120
+
121
+ ### `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
122
+
123
+ Lower-level glyph-extraction primitives everything else in this library is built on, exported for direct use. See `src/glyphs.ts` for the exact xTool JSON conventions these were reverse-engineered against.
124
+
125
+ ### Building (`src/builder.ts`, `src/layout.ts`)
126
+
127
+ `createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). Chainable methods:
128
+
129
+ | Method | Adds |
130
+ |--------|------|
131
+ | `.addText(text, x, y, options?)` | A `TEXT` display. Pass `options.layout` (see below) for real glyph outlines — without it, text renders as placeholder blocks in xTool Studio. |
132
+ | `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
133
+ | `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
134
+ | `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
135
+
136
+ Then `.generate()` (the plain `XCSFile` object), `.toJSON()` (string), or `.toBytes()` (`Uint8Array`, matching `renderXcsFile`'s output type).
137
+
138
+ Text layout — build the `layout` option for `.addText()` with one of:
139
+
140
+ | Function | Layout |
141
+ |----------|--------|
142
+ | `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
143
+ | `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
144
+ | `layoutCurvedGlyphText(font, text, emSizeMm, curveDeg, boxWidthMm, boxHeightMm, align?, letterSpacingMm?)` | Single-line, curved along an arc spanning `curveDeg` degrees across a `boxWidthMm`-wide box (positive arches the apex upward, negative bows it downward — same convention as SVG `<textPath>`/canvas curved-text editors). Multi-line curved text isn't supported (matches typical curved-text editors, which also flatten multi-line curved text to one line). **Not yet visually verified against real xTool Studio rendering** (there's no way to check outside the application itself) — the geometry is self-consistent and unit-tested, but treat curved output as best-effort until you've spot-checked it. |
145
+
146
+ All three return `GlyphTextLayout | null` (`null` for empty/whitespace-only text). `translateGlyphLayout(layout, dxMm, dyMm)` cheaply shifts an already-built layout without relaying out the font.
147
+
148
+ ## Development
149
+
150
+ ```bash
151
+ pnpm install # install dev dependencies
152
+ pnpm build # compile → dist/ (ESM + CJS + .d.ts)
153
+ pnpm dev # watch mode — rebuilds on save
154
+ pnpm test # run tests with vitest
155
+ npx tsc --noEmit # type-check without emitting
156
+ ```
157
+
158
+ Output files after `pnpm build`:
159
+
160
+ | File | Format |
161
+ |------|--------|
162
+ | `dist/index.js` | ESM |
163
+ | `dist/index.cjs` | CommonJS |
164
+ | `dist/index.d.ts` | TypeScript declarations |
165
+
166
+ ### Releasing
167
+
168
+ Publishing to npm is automated. Bump the version in `package.json`, add a
169
+ `CHANGELOG.md` entry, then create a GitHub release tagged `vX.Y.Z` — the
170
+ [publish workflow](.github/workflows/publish.yml) checks the tag against
171
+ `package.json`, rebuilds, runs the tests, and publishes with provenance.
172
+ It needs an `NPM_TOKEN` repository secret with publish rights.
173
+
174
+ ## Notes
175
+
176
+ - All inputs and outputs use plain browser-compatible types (`ArrayBuffer`, `Uint8Array`, `string`) — no Node.js APIs, no DOM, no framework required. (`atob`/`crypto.randomUUID` are used for the bundled font and glyph IDs; both are available in browsers and Node ≥ 19.)
177
+ - Only `TEXT` display objects are modified during substitution. Geometry, bitmaps, device configuration, and all other fields pass through unchanged.
178
+ - This library targets the current xTool Creative Space JSON format. Older versions of the software may export a different structure.
179
+ - Glyph outline extraction is powered by [opentype.js](https://github.com/opentypejs/opentype.js). The bundled fallback font is [Arimo](https://fonts.google.com/specimen/Arimo) v5.2.8 via [`@fontsource/arimo`](https://www.npmjs.com/package/@fontsource/arimo) (Apache License 2.0, see `third_party/arimo/LICENSE`).
180
+
181
+ ## License
182
+
183
+ [MIT](LICENSE). The bundled Arimo fallback font is separately licensed under Apache-2.0 (`third_party/arimo/LICENSE`).
184
+
185
+ ## Buy Me a Coffee
186
+
187
+ If this app, code, or repository has helped you or someone you know, please consider donating. I appreciate any help to offset the costs of development and/or AI Credits.
188
+
189
+ [**Donate via Stripe**](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800), or scan:
190
+
191
+ [![Donate via Stripe](./donate.svg)](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)