@richardmcquiston01/unofficial-xcs-writer 0.8.0 → 0.8.2

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 (4) hide show
  1. package/API.md +118 -0
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +105 -182
  4. package/package.json +3 -2
package/API.md ADDED
@@ -0,0 +1,118 @@
1
+ # API Reference
2
+
3
+ [Back to README](./README.md)
4
+
5
+ | [`assertXcsFormat(buffer: ArrayBuffer): void`](#assertxcsformatbuffer-arraybuffer-void) | [`readXcsFile(buffer: ArrayBuffer): string`](#readxcsfilebuffer-arraybuffer-string) | [`extractXcsTokens(buffer: ArrayBuffer): string[]`](#extractxcstokensbuffer-arraybuffer-string) | [`renderXcsFile(buffer, variables, values, fonts?): Uint8Array`](#renderxcsfilebuffer-variables-values-fonts-uint8array) | [`XcsVariable`](#xcsvariable) | [`.xs` (v2 workspace) format](#xs-v2-workspace-format) | [`assertXsFormat(buffer: ArrayBuffer): void`](#assertxsformatbuffer-arraybuffer-void) | [`extractXsTokens(buffer: ArrayBuffer): string[]`](#extractxstokensbuffer-arraybuffer-string) | [`renderXsFile(buffer, variables, values, fonts?): Uint8Array`](#renderxsfilebuffer-variables-values-fonts-uint8array) | [`buildXsArchive(file: XCSFile): Uint8Array`](#buildxsarchivefile-xcsfile-uint8array) | [`loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`](#loadfontbuffer-arraybuffer--loaddefaultfont--layouttextfont-text-originx-originy) | [Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)](#building-srcbuilderts-srclayoutts-srcmachinests) |
6
+
7
+ ## `assertXcsFormat(buffer: ArrayBuffer): void`
8
+
9
+ Validates that `buffer` is a parseable XCS JSON file containing a `canvas` array. Throws a descriptive error if the file is invalid or unrecognized.
10
+
11
+ [Back to Top](#api-reference)
12
+
13
+ ## `readXcsFile(buffer: ArrayBuffer): string`
14
+
15
+ Decodes the buffer and returns the raw JSON string. Useful for inspection or pass-through scenarios.
16
+
17
+ [Back to Top](#api-reference)
18
+
19
+ ## `extractXcsTokens(buffer: ArrayBuffer): string[]`
20
+
21
+ 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.
22
+
23
+ [Back to Top](#api-reference)
24
+
25
+ ## `renderXcsFile(buffer, variables, values, fonts?): Uint8Array`
26
+
27
+ 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.
28
+
29
+ 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.
30
+
31
+ Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
32
+
33
+ ```ts
34
+ const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
35
+ renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
36
+ ```
37
+
38
+ 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.
39
+
40
+ **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.
41
+
42
+ [Back to Top](#api-reference)
43
+
44
+ ## `XcsVariable`
45
+
46
+ ```ts
47
+ interface XcsVariable {
48
+ token: string; // token name without braces, e.g. "LastName"
49
+ defaultValue?: string;
50
+ }
51
+ ```
52
+
53
+ [Back to Top](#api-reference)
54
+
55
+ ## `.xs` (v2 workspace) format
56
+
57
+ [Back to Top](#api-reference)
58
+
59
+ ## `assertXsFormat(buffer: ArrayBuffer): void`
60
+
61
+ Validates that `buffer` is a ZIP archive with a `.format` entry equal to `"v2"` and a `project.json` entry. Throws a descriptive error otherwise.
62
+
63
+ [Back to Top](#api-reference)
64
+
65
+ ## `extractXsTokens(buffer: ArrayBuffer): string[]`
66
+
67
+ Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunkIndex>.json` chunk across the archive instead of a single `canvas` array.
68
+
69
+ [Back to Top](#api-reference)
70
+
71
+ ## `renderXsFile(buffer, variables, values, fonts?): Uint8Array`
72
+
73
+ Same substitution and glyph-regeneration behavior as `renderXcsFile` (see above — including the same `fonts` parameter and curved-text limitation), applied to every displays chunk. Returns a re-zipped `.xs` archive; every entry other than the modified displays chunks is passed through byte-for-byte unchanged.
74
+
75
+ [Back to Top](#api-reference)
76
+
77
+ ## `buildXsArchive(file: XCSFile): Uint8Array`
78
+
79
+ Packages an already-`generate()`d project (see Building, below) into a `.xs` v2 ZIP archive. You won't normally call this directly — use `XCSGenerator.toXsBytes()` instead, which calls it for you.
80
+
81
+ [Back to Top](#api-reference)
82
+
83
+ ## `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
84
+
85
+ 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.
86
+
87
+ [Back to Top](#api-reference)
88
+
89
+ ## Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)
90
+
91
+ `createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). `deviceId` (and `options.deviceId`) accepts a `XTOOL_MACHINES` key, a `MachineProfile` object, or a raw device id string — see "Building a project from scratch" in the [README](./README.md). Chainable methods:
92
+
93
+ | Method | Adds |
94
+ |--------|------|
95
+ | `.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. |
96
+ | `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
97
+ | `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
98
+ | `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
99
+
100
+ `.addText()`/`.addPath()`/`.addBitmap()` all also accept a `processing: { processingType, values }` option — see "Building a project from scratch" in the [README](./README.md); it only affects `.toXsBytes()` output.
101
+
102
+ Then `.generate()` (the plain `XCSFile` object), `.toJSON()` (string), `.toBytes()` (`Uint8Array`, matching `renderXcsFile`'s output type, as `.xcs`), or `.toXsBytes()` (`Uint8Array`, matching `renderXsFile`'s output type, as `.xs`).
103
+
104
+ Text layout — build the `layout` option for `.addText()` with one of:
105
+
106
+ | Function | Layout |
107
+ |----------|--------|
108
+ | `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
109
+ | `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
110
+ | `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. |
111
+
112
+ 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.
113
+
114
+ [Back to Top](#api-reference)
115
+
116
+ ---
117
+
118
+ [Back to README](./README.md)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ [Back to README](./README.md)
4
+
5
+ | [Unreleased](#unreleased) | [0.8.2 - 2026-10-13](#082---2026-10-13) | [0.8.1 - 2026-09-21](#081---2026-09-21) | [0.8.0 - 2026-09-21](#080---2026-09-21) | [0.7.0 - 2026-09-21](#070---2026-09-21) | [0.6.0 - 2026-09-21](#060---2026-09-21) | [0.5.0 - 2026-09-20](#050---2026-09-20) | [0.4.1 - 2026-09-19](#041---2026-09-19) | [0.4.0 - 2026-09-19](#040---2026-09-19) | [0.3.1 - 2026-08-17](#031---2026-08-17) | [0.3.0 - 2026-07-14](#030---2026-07-14) | [0.2.0 - 2026-07-14](#020---2026-07-14) | [0.1.0 - 2026-05-19](#010---2026-05-19) |
6
+
3
7
  All notable changes to this project will be documented in this file.
4
8
 
5
9
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
@@ -7,6 +11,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
11
 
8
12
  ## [Unreleased]
9
13
 
14
+ [Back to Top](#changelog)
15
+
16
+ ## [0.8.2] - 2026-10-13
17
+
18
+ ### Changed
19
+
20
+ - **Documentation only**: no library code changed. Rewrote `README.md` to be shorter, with an
21
+ author block, a Table of Contents, Back to Top links, a Supported Machines table, and the
22
+ License section corrected to MIT. Added a Back to README link, a menu row, and Back to Top
23
+ links to `API.md` and `CHANGELOG.md`.
24
+
25
+ [Back to Top](#changelog)
26
+
27
+ ## [0.8.1] - 2026-09-21
28
+
29
+ ### Changed
30
+
31
+ - **README restructure**: removed the "Releasing" subsection (internal maintainer process,
32
+ not user-facing); moved the full "API" section into a new `API.md` (added to `package.json`'s
33
+ `files` so it ships with the npm package), with the README's "API" section now just a link to
34
+ it; added a disclaimer blockquote under the title stating this is an unofficial,
35
+ community-built library not affiliated with xTool; removed the hyperlink on "xTool Creative
36
+ Space" in the intro paragraph (kept as plain text); moved "Buy Me a Coffee" up to sit directly
37
+ above the API section instead of at the very end.
38
+ - **Copyright section**: added a note that xTool, xTool Studio, and xTool Creative Space are
39
+ registered trademarks of Makeblock Co., Ltd., and that this project isn't affiliated with,
40
+ endorsed by, or sponsored by Makeblock or xTool.
41
+
42
+ [Back to Top](#changelog)
43
+
10
44
  ## [0.8.0] - 2026-09-21
11
45
 
12
46
  ### Added
@@ -27,6 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
27
61
  (`file.device.id`, i.e. `"P2S"`). `.xcs` output (`.toBytes()`) is unaffected -- it has no
28
62
  `deviceCode` field.
29
63
 
64
+ [Back to Top](#changelog)
65
+
30
66
  ## [0.7.0] - 2026-09-21
31
67
 
32
68
  ### Added
@@ -41,6 +77,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
41
77
  is structurally different and much less understood, so it isn't implemented (see `CLAUDE.md`'s
42
78
  Open Issues).
43
79
 
80
+ [Back to Top](#changelog)
81
+
44
82
  ## [0.6.0] - 2026-09-21
45
83
 
46
84
  ### Added
@@ -55,6 +93,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55
93
  generated `.xs` file's power/speed settings may need to be configured manually in xTool
56
94
  Studio before cutting/engraving.
57
95
 
96
+ [Back to Top](#changelog)
97
+
58
98
  ## [0.5.0] - 2026-09-20
59
99
 
60
100
  ### Added
@@ -72,6 +112,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
72
112
  vector store. `.xs` *generation* (building a new project from scratch) isn't implemented yet
73
113
  — see `CLAUDE.md`'s "Open Issues".
74
114
 
115
+ [Back to Top](#changelog)
116
+
75
117
  ## [0.4.1] - 2026-09-19
76
118
 
77
119
  ### Changed
@@ -82,6 +124,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
82
124
  from 0.4.0; this is the first published release carrying the generation API
83
125
  and curved-text layout described below.
84
126
 
127
+ [Back to Top](#changelog)
128
+
85
129
  ## [0.4.0] - 2026-09-19
86
130
 
87
131
  ### Added
@@ -109,6 +153,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
109
153
  still can't decode an existing curved display's `style.curveX`/`curveY`, but the new building
110
154
  API doesn't have that problem (the caller supplies the angle directly).
111
155
 
156
+ [Back to Top](#changelog)
157
+
112
158
  ## [0.3.1] - 2026-08-17
113
159
 
114
160
  ### Changed
@@ -119,6 +165,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
119
165
  - `donate.svg` is included in the published tarball so the README's QR image
120
166
  resolves for consumers reading it from the package.
121
167
 
168
+ [Back to Top](#changelog)
169
+
122
170
  ## [0.3.0] - 2026-07-14
123
171
 
124
172
  ### Added
@@ -134,6 +182,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
134
182
  0.2.0 tarball on npm was packed from a stale `dist/` and does not actually
135
183
  contain the glyph regeneration code its README describes — use 0.3.0 instead.
136
184
 
185
+ [Back to Top](#changelog)
186
+
137
187
  ## [0.2.0] - 2026-07-14
138
188
 
139
189
  > **Warning:** the npm tarball for this version was built from a stale `dist/`
@@ -159,6 +209,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
159
209
  substituted text on a curved TEXT display gets correctly-shaped glyphs laid
160
210
  out on a straight baseline.
161
211
 
212
+ [Back to Top](#changelog)
213
+
162
214
  ## [0.1.0] - 2026-05-19
163
215
 
164
216
  ### Added
@@ -168,3 +220,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
168
220
  - Core API: `assertXcsFormat`, `readXcsFile`, `extractXcsTokens`,
169
221
  `renderXcsFile`, and the `XcsVariable` type.
170
222
  - Dual ESM + CJS build with TypeScript declarations via tsup.
223
+
224
+ [Back to Top](#changelog)
225
+
226
+ ---
227
+
228
+ [Back to README](./README.md)
package/README.md CHANGED
@@ -1,21 +1,43 @@
1
1
  # unofficial-xcs-writer
2
2
 
3
- A framework-agnostic TypeScript library for reading, writing, building, and applying variable substitution to `.xcs` and `.xs` files produced by [xTool Creative Space](https://www.xtool.com/pages/software). Not affiliated with xTool.
3
+ - Author: [Richard McQuiston](mailto:richard.mcquiston@gmail.com)
4
+ - Website: https://richardmcquiston.com/
4
5
 
5
- ## What is an XCS / XS file?
6
+ > **Disclaimer:** This is an unofficial, community-built library. It is not affiliated with, endorsed by, or sponsored by xTool or Makeblock.
6
7
 
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.
8
+ ## Table of Contents
8
9
 
9
- Starting with xTool Studio v1.7, new projects are saved as `.xs` instead — a ZIP archive of several JSON files (project metadata, per-canvas display chunks, processing profiles, device bindings, and binary resources as real files rather than inline base64). `.xcs` is now legacy: v1.7+ can still open `.xcs` files but can no longer save back to that format. See `src/xs.ts` for the full format writeup.
10
+ | [Overview](#overview) | [Features](#features) | [Getting Started](#getting-started) | [Supported Machines](#supported-machines) | [Known Limitations](#known-limitations) | [More Information](#more-information) | [Development](#development) | [Buy Me a Coffee](#buy-me-a-coffee) | [License](#license) | [Copyright](#copyright) |
10
11
 
11
- This library lets you, for both formats:
12
+ ## Overview
12
13
 
13
- - **Read** a 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
- - **Build** a new project from scratch — text (straight, multi-line, or curved), paths, and embedded images — and export it as either `.xcs` or `.xs`
14
+ This is a TypeScript library for xTool project files. It reads, builds, and fills in templates for `.xcs` and `.xs` files.
17
15
 
18
- ## Installation
16
+ An `.xcs` file is plain JSON. An `.xs` file is a ZIP archive of JSON files. xTool Studio 1.7 and newer saves `.xs` by default. It can still open `.xcs` files but can no longer save them.
17
+
18
+ The library uses no framework, no DOM, and no Node.js-only features. It works in browsers and in Node.js.
19
+
20
+ [Back to Top](#table-of-contents)
21
+
22
+ ## Features
23
+
24
+ - Find `{{token}}` placeholders in text objects.
25
+ - Fill in the placeholders with your own values. The library draws new glyph outlines for the changed text.
26
+ - Build a project from scratch. You can add straight, multi-line, or curved text, paths, and images.
27
+ - Save the project as `.xcs` or `.xs`.
28
+ - Pick a target machine from a list of 13 xTool devices.
29
+ - Set power and speed for each object in `.xs` output.
30
+
31
+ [Back to Top](#table-of-contents)
32
+
33
+ ## Getting Started
34
+
35
+ ### Prerequisites
36
+
37
+ - Node.js 19 or newer, or a current web browser.
38
+ - A package manager such as npm or pnpm.
39
+
40
+ ### Installation
19
41
 
20
42
  ```bash
21
43
  npm install @richardmcquiston01/unofficial-xcs-writer
@@ -23,49 +45,46 @@ npm install @richardmcquiston01/unofficial-xcs-writer
23
45
  pnpm add @richardmcquiston01/unofficial-xcs-writer
24
46
  ```
25
47
 
26
- ## Usage
48
+ ### Usage
49
+
50
+ To fill in a template, follow these steps:
51
+
52
+ 1. Load the file as an `ArrayBuffer`.
53
+ 2. Check the format with `assertXcsFormat`. It throws an error if the file is not valid.
54
+ 3. List the placeholders with `extractXcsTokens`.
55
+ 4. Call `renderXcsFile` with your values.
56
+
57
+ For `.xs` files, use `assertXsFormat`, `extractXsTokens`, and `renderXsFile`. They work the same way.
58
+
59
+ To build a project, call `createXCS` with a machine name. Add objects with `.addText()`, `.addPath()`, and `.addBitmap()`. Finish with `.toBytes()` for `.xcs` or `.toXsBytes()` for `.xs`.
60
+
61
+ ### Examples
62
+
63
+ Fill in a template:
27
64
 
28
65
  ```ts
29
- import { assertXcsFormat, extractXcsTokens, renderXcsFile } from '@richardmcquiston01/unofficial-xcs-writer';
66
+ import {
67
+ assertXcsFormat,
68
+ extractXcsTokens,
69
+ renderXcsFile,
70
+ } from '@richardmcquiston01/unofficial-xcs-writer';
30
71
  import type { XcsVariable } from '@richardmcquiston01/unofficial-xcs-writer';
31
72
 
32
- // Load an .xcs file as an ArrayBuffer (browser or Node.js)
33
- const buffer = await fetch('/templates/name-tag.xcs').then(r => r.arrayBuffer());
73
+ const buffer: ArrayBuffer = await fetch('/templates/name-tag.xcs').then((r) => r.arrayBuffer());
34
74
 
35
- // Validate it's a real XCS file
36
- assertXcsFormat(buffer); // throws if invalid
75
+ assertXcsFormat(buffer);
76
+ const tokens: string[] = extractXcsTokens(buffer); // ['FirstName', 'LastName']
37
77
 
38
- // Discover which {{token}} placeholders are in the file
39
- const tokens = extractXcsTokens(buffer);
40
- // e.g. ['FirstName', 'LastName']
41
-
42
- // Define variables and supply values
43
78
  const variables: XcsVariable[] = [
44
79
  { token: 'FirstName', defaultValue: 'Guest' },
45
80
  { token: 'LastName' },
46
81
  ];
47
- const values = { FirstName: 'Jane', LastName: 'Smith' };
82
+ const values: Record<string, string> = { FirstName: 'Jane', LastName: 'Smith' };
48
83
 
49
- // Produce a filled-in .xcs file ready for download
50
84
  const output: Uint8Array = renderXcsFile(buffer, variables, values);
51
85
  ```
52
86
 
53
- ### Working with `.xs` files
54
-
55
- Same shape, different functions — `.xs` is a ZIP archive, so there's no `readXsFile` equivalent to `readXcsFile` (there's no single JSON string to return), but token extraction and substitution work the same way:
56
-
57
- ```ts
58
- import { assertXsFormat, extractXsTokens, renderXsFile } from '@richardmcquiston01/unofficial-xcs-writer';
59
-
60
- const buffer = await fetch('/templates/name-tag.xs').then(r => r.arrayBuffer());
61
-
62
- assertXsFormat(buffer); // throws if invalid
63
- const tokens = extractXsTokens(buffer); // e.g. ['FirstName', 'LastName']
64
-
65
- const output: Uint8Array = renderXsFile(buffer, variables, values);
66
- ```
67
-
68
- ### Building a project from scratch
87
+ Build a project from scratch:
69
88
 
70
89
  ```ts
71
90
  import {
@@ -77,192 +96,96 @@ import {
77
96
 
78
97
  const font = loadDefaultFont();
79
98
 
80
- const project = createXCS('P2S')
81
- // Straight text needs a real glyph layout so xTool Studio renders
82
- // actual outlines instead of placeholder blocks.
99
+ const bytes: Uint8Array = createXCS('P2S')
83
100
  .addText('Hello', 10, 10, {
84
101
  fontFamily: 'Arial',
85
102
  fontSize: 24,
86
103
  layout: layoutGlyphText(font, 'Hello', 8, 10, 10),
87
104
  })
88
- // Curved text: same idea, via layoutCurvedGlyphText -- the curve
89
- // is baked into each character's own glyph shape and position, the
90
- // way substituted text already bakes straight shapes.
91
105
  .addText('Arch Text', 0, 40, {
92
106
  fontFamily: 'Arial',
93
107
  fontSize: 18,
94
108
  layout: layoutCurvedGlyphText(font, 'Arch Text', 6, 90, 60, 20, 'center'),
95
109
  })
96
110
  .addPath('M0 0L10 0L10 10L0 10Z', 0, 60, 10, 10, {
97
- // Only affects .toXsBytes() output -- see below.
98
111
  processing: { processingType: 'VECTOR_CUTTING', values: { power: 80, speed: 10 } },
99
112
  })
100
- .toBytes(); // Uint8Array, ready to write to a .xcs file
113
+ .toXsBytes();
101
114
  ```
102
115
 
103
- Call `.toXsBytes()` instead of `.toBytes()` to export the same project as a `.xs` (v2 workspace)
104
- archive — same chainable builder, same display objects, different container.
105
-
106
- Pass `processing: { processingType, values }` to `.addText()`/`.addPath()`/`.addBitmap()`'s
107
- options to set that display's power/speed/etc. in the exported `.xs` file's `profiles.json` +
108
- device bindings — `.xcs` output (`.toBytes()`) ignores this option; its own per-display
109
- processing format is different and not supported yet. Displays given identical settings share
110
- one profile/binding automatically. `processingType`/`values`' exact shape is xTool Studio's own
111
- and undocumented beyond real exports — see `xs_samples/*.xs`'s `profiles.json` for examples
112
- (`VECTOR_ENGRAVING`, `VECTOR_CUTTING`, `FILL_VECTOR_ENGRAVING`). Displays with no `processing`
113
- option behave as before: a generated `.xs` file's power/speed may need to be set manually in
114
- xTool Studio before cutting/engraving.
115
-
116
- `createXCS('P2S')`'s argument (and `XCSGeneratorOptions.deviceId`) accepts a known machine name
117
- from `XTOOL_MACHINES` — currently 13 machines verified from real exports in this repo (`"P2S"`,
118
- `"F2 Ultra UV"`, `"S1"`, `"P3"`, `"M2"`, `"F2"`, `"F2 Ultra (Single)"`, `"F2 Ultra"`,
119
- `"M1 Ultra"`, `"F1"`, `"F1 Lite"`, `"F1 Ultra"`, `"MetalFab"`) — and resolves the real
120
- `extId`/`extName`/`deviceCode` (`.xs` only)/default power for that machine, instead of reusing
121
- whatever string you pass for all of them. Export the same design for multiple machines by
122
- building it once and calling `.toXsBytes()`/`.toBytes()` once per `createXCS(machineName)`:
116
+ Keep these points in mind:
123
117
 
124
- ```ts
125
- import { createXCS, XTOOL_MACHINES } from '@richardmcquiston01/unofficial-xcs-writer';
126
-
127
- for (const machine of Object.keys(XTOOL_MACHINES)) {
128
- const bytes = createXCS(machine).addPath('M0 0L10 0L10 10L0 10Z', 0, 0, 10, 10).toXsBytes();
129
- // write `bytes` to e.g. `design-${machine}.xs`
130
- }
131
- ```
132
-
133
- A device id not in the catalog still works exactly as before (no `.xs` `deviceCode`, `extId`/
134
- `extName`/`device.id` all set to that string, default power `55`); you can also pass a
135
- `MachineProfile` object directly for a machine not yet in the catalog.
118
+ - Text needs a glyph `layout`. Without one, xTool Studio shows placeholder blocks instead of real letters.
119
+ - The `processing` option only changes `.xs` output. Objects with the same settings share one profile.
120
+ - The library does not check the processing values. Confirm power and speed in xTool Studio before you cut.
136
121
 
137
- ## API
122
+ [Back to Top](#table-of-contents)
138
123
 
139
- ### `assertXcsFormat(buffer: ArrayBuffer): void`
124
+ ## Supported Machines
140
125
 
141
- Validates that `buffer` is a parseable XCS JSON file containing a `canvas` array. Throws a descriptive error if the file is invalid or unrecognized.
126
+ Pass one of these names to `createXCS`. The library then uses the real device ids and default power for that machine.
142
127
 
143
- ### `readXcsFile(buffer: ArrayBuffer): string`
128
+ | | | | |
129
+ |---|---|---|---|
130
+ | P2S | P3 | S1 | M2 |
131
+ | M1 Ultra | F1 | F1 Lite | F1 Ultra |
132
+ | F2 | F2 Ultra | F2 Ultra (Single) | F2 Ultra UV |
133
+ | MetalFab | | | |
144
134
 
145
- Decodes the buffer and returns the raw JSON string. Useful for inspection or pass-through scenarios.
135
+ You can also pass a `MachineProfile` object or a plain device id string. A plain string still works, but it gets default values. To export one design for every machine, loop over `Object.keys(XTOOL_MACHINES)`.
146
136
 
147
- ### `extractXcsTokens(buffer: ArrayBuffer): string[]`
137
+ [Back to Top](#table-of-contents)
148
138
 
149
- 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.
139
+ ## Known Limitations
150
140
 
151
- ### `renderXcsFile(buffer, variables, values, fonts?): Uint8Array`
141
+ - New text on an already curved text object is laid out straight.
142
+ - Curved text built from scratch has not been checked against real xTool Studio output.
143
+ - Multi-line curved text is not supported.
144
+ - `.xcs` output has no processing-profile support.
152
145
 
153
- 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.
146
+ [Back to Top](#table-of-contents)
154
147
 
155
- 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.
156
-
157
- Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
158
-
159
- ```ts
160
- const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
161
- renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
162
- ```
148
+ ## More Information
163
149
 
164
- 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.
150
+ - [API.md](./API.md) lists every function, option, and type.
151
+ - [CHANGELOG.md](./CHANGELOG.md) lists changes for each release.
165
152
 
166
- **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.
167
-
168
- ### `XcsVariable`
169
-
170
- ```ts
171
- interface XcsVariable {
172
- token: string; // token name without braces, e.g. "LastName"
173
- defaultValue?: string;
174
- }
175
- ```
176
-
177
- ### `.xs` (v2 workspace) format
178
-
179
- ### `assertXsFormat(buffer: ArrayBuffer): void`
180
-
181
- Validates that `buffer` is a ZIP archive with a `.format` entry equal to `"v2"` and a `project.json` entry. Throws a descriptive error otherwise.
182
-
183
- ### `extractXsTokens(buffer: ArrayBuffer): string[]`
184
-
185
- Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunkIndex>.json` chunk across the archive instead of a single `canvas` array.
186
-
187
- ### `renderXsFile(buffer, variables, values, fonts?): Uint8Array`
188
-
189
- Same substitution and glyph-regeneration behavior as `renderXcsFile` (see above — including the same `fonts` parameter and curved-text limitation), applied to every displays chunk. Returns a re-zipped `.xs` archive; every entry other than the modified displays chunks is passed through byte-for-byte unchanged.
190
-
191
- ### `buildXsArchive(file: XCSFile): Uint8Array`
192
-
193
- Packages an already-`generate()`d project (see Building, below) into a `.xs` v2 ZIP archive. You won't normally call this directly — use `XCSGenerator.toXsBytes()` instead, which calls it for you.
194
-
195
- ### `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
196
-
197
- 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.
198
-
199
- ### Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)
200
-
201
- `createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). `deviceId` (and `options.deviceId`) accepts a `XTOOL_MACHINES` key, a `MachineProfile` object, or a raw device id string — see "Building a project from scratch" above. Chainable methods:
202
-
203
- | Method | Adds |
204
- |--------|------|
205
- | `.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. |
206
- | `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
207
- | `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
208
- | `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
209
-
210
- `.addText()`/`.addPath()`/`.addBitmap()` all also accept a `processing: { processingType, values }` option — see "Building a project from scratch" above; it only affects `.toXsBytes()` output.
211
-
212
- Then `.generate()` (the plain `XCSFile` object), `.toJSON()` (string), `.toBytes()` (`Uint8Array`, matching `renderXcsFile`'s output type, as `.xcs`), or `.toXsBytes()` (`Uint8Array`, matching `renderXsFile`'s output type, as `.xs`).
213
-
214
- Text layout — build the `layout` option for `.addText()` with one of:
215
-
216
- | Function | Layout |
217
- |----------|--------|
218
- | `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
219
- | `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
220
- | `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. |
221
-
222
- 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.
153
+ [Back to Top](#table-of-contents)
223
154
 
224
155
  ## Development
225
156
 
226
157
  ```bash
227
- pnpm install # install dev dependencies
228
- pnpm build # compile → dist/ (ESM + CJS + .d.ts)
229
- pnpm dev # watch mode — rebuilds on save
230
- pnpm test # run tests with vitest
231
- npx tsc --noEmit # type-check without emitting
158
+ pnpm install # install dependencies
159
+ pnpm build # build to dist/ (ESM, CJS, and type files)
160
+ pnpm dev # rebuild on every save
161
+ pnpm test # run the tests
162
+ pnpm typecheck # check types without building
232
163
  ```
233
164
 
234
- Output files after `pnpm build`:
165
+ The library uses [opentype.js](https://github.com/opentypejs/opentype.js) to read glyph outlines and [fflate](https://github.com/101arrowz/fflate) to read and write ZIP archives. The built-in fallback font is [Arimo](https://fonts.google.com/specimen/Arimo) version 5.2.8, under the Apache 2 license. Its license text is in `third_party/arimo/LICENSE`.
235
166
 
236
- | File | Format |
237
- |------|--------|
238
- | `dist/index.js` | ESM |
239
- | `dist/index.cjs` | CommonJS |
240
- | `dist/index.d.ts` | TypeScript declarations |
167
+ [Back to Top](#table-of-contents)
168
+
169
+ ## Buy Me a Coffee
241
170
 
242
- ### Releasing
171
+ 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.
243
172
 
244
- Publishing to npm is automated. Bump the version in `package.json`, add a
245
- `CHANGELOG.md` entry, then create a GitHub release tagged `vX.Y.Z` — the
246
- [publish workflow](.github/workflows/publish.yml) checks the tag against
247
- `package.json`, rebuilds, runs the tests, and publishes with provenance.
248
- It needs an `NPM_TOKEN` repository secret with publish rights.
173
+ [**Donate via Stripe**](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800), or scan:
249
174
 
250
- ## Notes
175
+ [![Donate via Stripe](./donate.svg)](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)
251
176
 
252
- - 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.)
253
- - Only `TEXT` display objects are modified during substitution. Geometry, bitmaps, device configuration, and all other fields pass through unchanged.
254
- - This library targets the current xTool Creative Space JSON/`.xs` formats. Older versions of the software may export a different structure.
255
- - `.xs` archives are read and re-written with [fflate](https://github.com/101arrowz/fflate); large `PATH` displays' vector data may be deduplicated into a separate `vectors/<bucketType>/` store rather than inlined — this library never touches `PATH` displays, so that structure round-trips untouched.
256
- - 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`).
177
+ [Back to Top](#table-of-contents)
257
178
 
258
179
  ## License
259
180
 
260
- [MIT](LICENSE). The bundled Arimo fallback font is separately licensed under Apache-2.0 (`third_party/arimo/LICENSE`).
181
+ MIT. See [LICENSE](./LICENSE). The bundled Arimo font uses the Apache 2 license.
261
182
 
262
- ## Buy Me a Coffee
183
+ [Back to Top](#table-of-contents)
263
184
 
264
- 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.
185
+ ## Copyright
265
186
 
266
- [**Donate via Stripe**](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800), or scan:
187
+ (c)2026 Richard McQuiston. All rights reserved.
267
188
 
268
- [![Donate via Stripe](./donate.svg)](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)
189
+ xTool, xTool Studio, and xTool Creative Space are trademarks of Makeblock Co., Ltd. This project is not affiliated with, endorsed by, or sponsored by Makeblock Co., Ltd. or xTool.
190
+
191
+ [Back to Top](#table-of-contents)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@richardmcquiston01/unofficial-xcs-writer",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Read, write, build, and apply variable substitution to xTool Creative Space .xcs and .xs files, with real glyph outline extraction for text",
5
5
  "license": "MIT",
6
6
  "author": "Richard McQuiston <richard.mcquiston01@gmail.com>",
@@ -33,7 +33,8 @@
33
33
  "dist",
34
34
  "third_party",
35
35
  "donate.svg",
36
- "CHANGELOG.md"
36
+ "CHANGELOG.md",
37
+ "API.md"
37
38
  ],
38
39
  "devDependencies": {
39
40
  "@types/node": "^26.6.2",